diff --git a/src/java.base/share/classes/jdk/internal/net/quic/QuicKeyUnavailableException.java b/src/java.base/share/classes/jdk/internal/net/quic/QuicKeyUnavailableException.java
new file mode 100644
index 00000000000..89d15eb3439
--- /dev/null
+++ b/src/java.base/share/classes/jdk/internal/net/quic/QuicKeyUnavailableException.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2024, 2025, 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. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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.internal.net.quic;
+
+
+import java.util.Objects;
+
+import jdk.internal.net.quic.QuicTLSEngine.KeySpace;
+
+/**
+ * Thrown when an operation on {@link QuicTLSEngine} doesn't have the necessary
+ * QUIC keys for encrypting or decrypting packets. This can either be because
+ * the keys aren't available for a particular {@linkplain KeySpace keyspace} or
+ * the keys for the {@code keyspace} have been discarded.
+ */
+public final class QuicKeyUnavailableException extends Exception {
+ @java.io.Serial
+ private static final long serialVersionUID = 8553365136999153478L;
+
+ public QuicKeyUnavailableException(final String message, final KeySpace keySpace) {
+ super(Objects.requireNonNull(keySpace) + " keyspace: " + message);
+ }
+}
diff --git a/src/java.base/share/classes/jdk/internal/net/quic/QuicOneRttContext.java b/src/java.base/share/classes/jdk/internal/net/quic/QuicOneRttContext.java
new file mode 100644
index 00000000000..fd0b405069c
--- /dev/null
+++ b/src/java.base/share/classes/jdk/internal/net/quic/QuicOneRttContext.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2023, 2025, 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. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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.internal.net.quic;
+
+/**
+ * Supplies contextual 1-RTT information that's available in the QUIC implementation of the
+ * {@code java.net.http} module, to the QUIC TLS layer in the {@code java.base} module.
+ */
+public interface QuicOneRttContext {
+
+ /**
+ * {@return the largest packet number that was acknowledged by
+ * the peer in the 1-RTT packet space}
+ */
+ long getLargestPeerAckedPN();
+}
diff --git a/src/java.base/share/classes/jdk/internal/net/quic/QuicTLSContext.java b/src/java.base/share/classes/jdk/internal/net/quic/QuicTLSContext.java
new file mode 100644
index 00000000000..5cf0c999fb9
--- /dev/null
+++ b/src/java.base/share/classes/jdk/internal/net/quic/QuicTLSContext.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (c) 2022, 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
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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.internal.net.quic;
+
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.VarHandle;
+import java.util.Arrays;
+import java.util.Objects;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLContextSpi;
+import javax.net.ssl.SSLParameters;
+
+import sun.security.ssl.QuicTLSEngineImpl;
+import sun.security.ssl.SSLContextImpl;
+
+/**
+ * Instances of this class act as a factory for creation
+ * of {@link QuicTLSEngine QUIC TLS engine}.
+ */
+public final class QuicTLSContext {
+
+ // In this implementation, we have a dependency on
+ // sun.security.ssl.SSLContextImpl. We can only support
+ // Quic on SSLContext instances created by the default
+ // SunJSSE Provider
+ private final SSLContextImpl sslCtxImpl;
+
+ /**
+ * {@return {@code true} if the given {@code sslContext} supports QUIC TLS, {@code false} otherwise}
+ * @param sslContext an {@link SSLContext}
+ */
+ public static boolean isQuicCompatible(final SSLContext sslContext) {
+ boolean parametersSupported = isQuicCompatible(sslContext.getSupportedSSLParameters());
+ if (!parametersSupported) {
+ return false;
+ }
+ // horrible hack - what we do here is try and get hold of a SSLContext
+ // that has already been initialised and configured with the HttpClient.
+ // We see if that SSLContext is created using an implementation of
+ // sun.security.ssl.SSLContextImpl. Since there's no API
+ // available to get hold of that underlying implementation, we use
+ // MethodHandle lookup to get access to the field which holds that
+ // detail.
+ final Object underlyingImpl = CONTEXT_SPI.get(sslContext);
+ if (!(underlyingImpl instanceof SSLContextImpl ssci)) {
+ return false;
+ }
+ return ssci.isUsableWithQuic();
+ }
+
+ /**
+ * {@return {@code true} if protocols of the given {@code parameters} support QUIC TLS, {@code false} otherwise}
+ */
+ public static boolean isQuicCompatible(SSLParameters parameters) {
+ String[] protocols = parameters.getProtocols();
+ return protocols != null && Arrays.asList(protocols).contains("TLSv1.3");
+ }
+
+ private static SSLContextImpl getSSLContextImpl(
+ final SSLContext sslContext) {
+ final Object underlyingImpl = CONTEXT_SPI.get(sslContext);
+ assert underlyingImpl instanceof SSLContextImpl;
+ return (SSLContextImpl) underlyingImpl;
+ }
+
+ /**
+ * Constructs a QuicTLSContext for the given {@code sslContext}
+ *
+ * @param sslContext The SSLContext
+ * @throws IllegalArgumentException If the passed {@code sslContext} isn't
+ * supported by the QuicTLSContext
+ * @see #isQuicCompatible(SSLContext)
+ */
+ public QuicTLSContext(final SSLContext sslContext) {
+ Objects.requireNonNull(sslContext);
+ if (!isQuicCompatible(sslContext)) {
+ throw new IllegalArgumentException(
+ "Cannot construct a QUIC TLS context with the given SSLContext");
+ }
+ this.sslCtxImpl = getSSLContextImpl(sslContext);
+ }
+
+ /**
+ * Creates a {@link QuicTLSEngine} using this context
+ *
+ * This method does not provide hints for session caching.
+ *
+ * @return the newly created QuicTLSEngine
+ */
+ public QuicTLSEngine createEngine() {
+ return createEngine(null, -1);
+ }
+
+ /**
+ * Creates a {@link QuicTLSEngine} using this context using
+ * advisory peer information.
+ *
+ * The provided parameters will be used as hints for session caching.
+ * The {@code peerHost} parameter will be used in the server_name extension,
+ * unless overridden later.
+ *
+ * @param peerHost The peer hostname or IP address. Can be null.
+ * @param peerPort The peer port, can be -1 if the port is unknown
+ * @return the newly created QuicTLSEngine
+ */
+ public QuicTLSEngine createEngine(final String peerHost, final int peerPort) {
+ return new QuicTLSEngineImpl(this.sslCtxImpl, peerHost, peerPort);
+ }
+
+ // This VarHandle is used to access the SSLContext::contextSpi
+ // field which is not publicly accessible.
+ // In this implementation, Quic is only supported for SSLContext
+ // instances whose underlying implementation is provided by a
+ // sun.security.ssl.SSLContextImpl
+ private static final VarHandle CONTEXT_SPI;
+ static {
+ try {
+ final MethodHandles.Lookup lookup =
+ MethodHandles.privateLookupIn(SSLContext.class,
+ MethodHandles.lookup());
+ final VarHandle vh = lookup.findVarHandle(SSLContext.class,
+ "contextSpi", SSLContextSpi.class);
+ CONTEXT_SPI = vh;
+ } catch (Exception x) {
+ throw new ExceptionInInitializerError(x);
+ }
+ }
+}
+
diff --git a/src/java.base/share/classes/jdk/internal/net/quic/QuicTLSEngine.java b/src/java.base/share/classes/jdk/internal/net/quic/QuicTLSEngine.java
new file mode 100644
index 00000000000..70ed86bbf01
--- /dev/null
+++ b/src/java.base/share/classes/jdk/internal/net/quic/QuicTLSEngine.java
@@ -0,0 +1,508 @@
+/*
+ * Copyright (c) 2021, 2025, 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. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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.internal.net.quic;
+
+import javax.crypto.AEADBadTagException;
+import javax.crypto.ShortBufferException;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLParameters;
+import javax.net.ssl.SSLSession;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Set;
+import java.util.function.IntFunction;
+
+/**
+ * One instance of these per QUIC connection. Configuration methods not shown
+ * but would be similar to SSLEngine.
+ */
+public interface QuicTLSEngine {
+
+ /**
+ * Represents the encryption level associated with a packet encryption or
+ * decryption. A QUIC connection has a current keyspace for sending and
+ * receiving which can be queried.
+ */
+ enum KeySpace {
+ INITIAL,
+ HANDSHAKE,
+ RETRY, // Special algorithm used for this packet
+ ZERO_RTT,
+ ONE_RTT
+ }
+
+ enum HandshakeState {
+ /**
+ * Need to receive a CRYPTO frame
+ */
+ NEED_RECV_CRYPTO,
+ /**
+ * Need to receive a HANDSHAKE_DONE frame from server to complete the
+ * handshake, but application data can be sent in this state (client
+ * only state).
+ */
+ NEED_RECV_HANDSHAKE_DONE,
+ /**
+ * Need to send a CRYPTO frame
+ */
+ NEED_SEND_CRYPTO,
+ /**
+ * Need to send a HANDSHAKE_DONE frame to complete the handshake, but
+ * application data can be sent in this state (server only state)
+ */
+ NEED_SEND_HANDSHAKE_DONE,
+ /**
+ * Need to execute a task
+ */
+ NEED_TASK,
+ /**
+ * Handshake is confirmed, as specified in section 4.1.2 of RFC-9001
+ */
+ // On client side this happens when client receives HANDSHAKE_DONE
+ // frame. On server side this happens when the TLS stack has both
+ // sent a Finished message and verified the peer's Finished message.
+ HANDSHAKE_CONFIRMED,
+ }
+
+ /**
+ * {@return the QUIC versions supported by this engine}
+ */
+ Set getSupportedQuicVersions();
+
+ /**
+ * If {@code mode} is {@code true} then configures this QuicTLSEngine to
+ * operate in client mode. If {@code false}, then this QuicTLSEngine
+ * operates in server mode.
+ *
+ * @param mode true to make this QuicTLSEngine operate in client
+ * mode, false otherwise
+ */
+ void setUseClientMode(boolean mode);
+
+ /**
+ * {@return true if this QuicTLSEngine is operating in client mode, false
+ * otherwise}
+ */
+ boolean getUseClientMode();
+
+ /**
+ * {@return the SSLParameters in effect for this engine.}
+ */
+ SSLParameters getSSLParameters();
+
+ /**
+ * Sets the {@code SSLParameters} to be used by this engine
+ *
+ * @param sslParameters the SSLParameters
+ * @throws IllegalArgumentException if
+ * {@linkplain SSLParameters#getProtocols() TLS protocol versions} on the
+ * {@code sslParameters} is either empty or contains anything other
+ * than {@code TLSv1.3}
+ * @throws NullPointerException if {@code sslParameters} is null
+ */
+ void setSSLParameters(SSLParameters sslParameters);
+
+ /**
+ * {@return the most recent application protocol value negotiated by the
+ * engine. Returns null if no application protocol has yet been negotiated
+ * by the engine}
+ */
+ String getApplicationProtocol();
+
+ /**
+ * {@return the SSLSession}
+ *
+ * @see SSLEngine#getSession()
+ */
+ SSLSession getSession();
+
+ /**
+ * Returns the SSLSession being constructed during a QUIC handshake.
+ *
+ * @return null if this instance is not currently handshaking, or if the
+ * current handshake has not progressed far enough to create
+ * a basic SSLSession. Otherwise, this method returns the
+ * {@code SSLSession} currently being negotiated.
+ *
+ * @see SSLEngine#getHandshakeSession()
+ */
+ SSLSession getHandshakeSession();
+
+ /**
+ * Returns the current handshake state of the connection. Sometimes packets
+ * that could be decrypted can be received before the handshake has
+ * completed, but should not be decrypted until it is complete
+ *
+ * @return the HandshakeState
+ */
+ HandshakeState getHandshakeState();
+
+ /**
+ * Returns true if the TLS handshake is considered complete.
+ *
+ * The TLS handshake is considered complete when the TLS stack
+ * has reported that the handshake is complete. This happens when
+ * the TLS stack has both sent a {@code Finished} message and verified
+ * the peer's {@code Finished} message.
+ *
+ * @return true if TLS handshake is complete, false otherwise.
+ */
+ boolean isTLSHandshakeComplete();
+
+ /**
+ * {@return the current sending key space (encryption level)}
+ */
+ KeySpace getCurrentSendKeySpace();
+
+ /**
+ * Checks whether the keys for the given key space are available.
+ *
+ * Keys are available when they are already computed and not discarded yet.
+ *
+ * @param keySpace key space to check
+ * @return true if the given keys are available
+ */
+ boolean keysAvailable(KeySpace keySpace);
+
+ /**
+ * Discard the keys used by the {@code keySpace}.
+ *
+ * Once the keys for a particular {@code keySpace} have been discarded, the
+ * keySpace will no longer be able to
+ * {@linkplain #encryptPacket(KeySpace, long, IntFunction,
+ * ByteBuffer, ByteBuffer) encrypt} or
+ * {@linkplain #decryptPacket(KeySpace, long, int, ByteBuffer, int, ByteBuffer)
+ * decrypt} packets.
+ *
+ * @param keySpace The keyspace whose current keys should be discarded
+ */
+ void discardKeys(KeySpace keySpace);
+
+ /**
+ * Provide quic_transport_parameters for inclusion in handshake message.
+ *
+ * @param params encoded quic_transport_parameters
+ */
+ void setLocalQuicTransportParameters(ByteBuffer params);
+
+ /**
+ * Reset the handshake state and produce a new ClientHello message.
+ *
+ * When a Quic client receives a Version Negotiation packet,
+ * it restarts the handshake by calling this method after updating the
+ * {@linkplain #setLocalQuicTransportParameters(ByteBuffer) transport parameters}
+ * with the new version information.
+ */
+ void restartHandshake() throws IOException;
+
+ /**
+ * Set consumer for quic_transport_parameters sent by the remote side.
+ * Consumer will receive a byte buffer containing the value of
+ * quic_transport_parameters extension sent by the remote endpoint.
+ *
+ * @param consumer consumer for remote quic transport parameters
+ */
+ void setRemoteQuicTransportParametersConsumer(
+ QuicTransportParametersConsumer consumer);
+
+ /**
+ * Derive initial keys for the given QUIC version and connection ID
+ * @param quicVersion QUIC protocol version
+ * @param connectionId initial destination connection ID
+ * @throws IllegalArgumentException if the {@code quicVersion} isn't
+ * {@linkplain #getSupportedQuicVersions() supported} on this
+ * {@code QuicTLSEngine}
+ */
+ void deriveInitialKeys(QuicVersion quicVersion, ByteBuffer connectionId) throws IOException;
+
+ /**
+ * Get the sample size for header protection algorithm
+ *
+ * @param keySpace Packet key space
+ * @return required sample size for header protection
+ * @throws IllegalArgumentException when keySpace does not require
+ * header protection
+ */
+ int getHeaderProtectionSampleSize(KeySpace keySpace);
+
+ /**
+ * Compute the header protection mask for the given sample,
+ * packet key space and direction (incoming/outgoing).
+ *
+ * @param keySpace Packet key space
+ * @param incoming true for incoming packets, false for outgoing
+ * @param sample sampled data
+ * @return mask bytes, at least 5.
+ * @throws IllegalArgumentException when keySpace does not require
+ * header protection or sample length is different from required
+ * @see #getHeaderProtectionSampleSize(KeySpace)
+ * @spec https://www.rfc-editor.org/rfc/rfc9001.html#name-header-protection-applicati
+ * RFC 9001, Section 5.4.1 Header Protection Application
+ */
+ ByteBuffer computeHeaderProtectionMask(KeySpace keySpace,
+ boolean incoming, ByteBuffer sample)
+ throws QuicKeyUnavailableException, QuicTransportException;
+
+ /**
+ * Get the authentication tag size. Encryption adds this number of bytes.
+ *
+ * @return authentication tag size
+ */
+ int getAuthTagSize();
+
+ /**
+ * Encrypt into {@code output}, the given {@code packetPayload} bytes using the
+ * keys for the given {@code keySpace}.
+ *
+ * Before encrypting the {@code packetPayload}, this method invokes the {@code headerGenerator}
+ * passing it the key phase corresponding to the encryption key that's in use.
+ * For {@code KeySpace}s where key phase isn't applicable, the {@code headerGenerator} will
+ * be invoked with a value of {@code 0} for the key phase.
+ *
+ * The {@code headerGenerator} is expected to return a {@code ByteBuffer} representing the
+ * packet header and where applicable, the returned header must contain the key phase
+ * that was passed to the {@code headerGenerator}. The packet header will be used as
+ * the Additional Authentication Data (AAD) for encrypting the {@code packetPayload}.
+ *
+ * Upon return, the {@code output} will contain the encrypted packet payload bytes
+ * and the authentication tag. The {@code packetPayload} and the packet header, returned
+ * by the {@code headerGenerator}, will have their {@code position} equal to their
+ * {@code limit}. The limit of either of those buffers will not have changed.
+ *
+ * It is recommended to do the encryption in place by using slices of a bigger
+ * buffer as the input and output buffer:
+ *
+ * +--------+-------------------+
+ * input: | header | plaintext payload |
+ * +--------+-------------------+----------+
+ * output: | encrypted payload | AEAD tag |
+ * +-------------------+----------+
+ *
+ *
+ * @param keySpace Packet key space
+ * @param packetNumber full packet number
+ * @param headerGenerator an {@link IntFunction} which takes a key phase and returns
+ * the packet header
+ * @param packetPayload buffer containing unencrypted packet payload
+ * @param output buffer into which the encrypted packet payload will be written
+ * @throws QuicKeyUnavailableException if keys are not available
+ * @throws QuicTransportException if encrypting the packet would result
+ * in exceeding the AEAD cipher confidentiality limit
+ */
+ void encryptPacket(KeySpace keySpace, long packetNumber,
+ IntFunction headerGenerator,
+ ByteBuffer packetPayload,
+ ByteBuffer output)
+ throws QuicKeyUnavailableException, QuicTransportException, ShortBufferException;
+
+ /**
+ * Decrypt the given packet bytes using keys for the given packet key space.
+ * Header protection must be removed before calling this method.
+ *
+ * The input buffer contains the packet header and the encrypted packet payload.
+ * The packet header (first {@code headerLength} bytes of the input buffer)
+ * is consumed by this method, but is not decrypted.
+ * The packet payload (bytes following the packet header) is decrypted
+ * by this method. This method consumes the entire input buffer.
+ *
+ * The decrypted payload bytes are written
+ * to the output buffer.
+ *
+ * It is recommended to do the decryption in place by using slices of a bigger
+ * buffer as the input and output buffer:
+ *
+ * +--------+-------------------+----------+
+ * input: | header | encrypted payload | AEAD tag |
+ * +--------+-------------------+----------+
+ * output: | decrypted payload |
+ * +-------------------+
+ *
+ *
+ * @param keySpace Packet key space
+ * @param packetNumber full packet number
+ * @param keyPhase key phase bit (0 or 1) found on the packet, or -1
+ * if the packet does not have a key phase bit
+ * @param packet buffer containing encrypted packet bytes
+ * @param headerLength length of the packet header
+ * @param output buffer where decrypted packet bytes will be stored
+ * @throws IllegalArgumentException if keyPhase bit is invalid
+ * @throws QuicKeyUnavailableException if keys are not available
+ * @throws AEADBadTagException if the provided packet's authentication tag
+ * is incorrect
+ * @throws QuicTransportException if decrypting the invalid packet resulted
+ * in exceeding the AEAD cipher integrity limit
+ */
+ void decryptPacket(KeySpace keySpace, long packetNumber, int keyPhase,
+ ByteBuffer packet, int headerLength, ByteBuffer output)
+ throws IllegalArgumentException, QuicKeyUnavailableException,
+ AEADBadTagException, QuicTransportException, ShortBufferException;
+
+ /**
+ * Sign the provided retry packet. Input buffer contains the retry packet
+ * payload. Integrity tag is stored in the output buffer.
+ *
+ * @param version Quic version
+ * @param originalConnectionId original destination connection ID,
+ * without length
+ * @param packet retry packet bytes without tag
+ * @param output buffer where integrity tag will be stored
+ * @throws ShortBufferException if output buffer is too short to
+ * hold the tag
+ * @throws IllegalArgumentException if originalConnectionId is
+ * longer than 255 bytes
+ * @throws IllegalArgumentException if {@code version} isn't
+ * {@linkplain #getSupportedQuicVersions() supported}
+ */
+ void signRetryPacket(QuicVersion version, ByteBuffer originalConnectionId,
+ ByteBuffer packet, ByteBuffer output) throws ShortBufferException, QuicTransportException;
+
+ /**
+ * Verify the provided retry packet.
+ *
+ * @param version Quic version
+ * @param originalConnectionId original destination connection ID,
+ * without length
+ * @param packet retry packet bytes with tag
+ * @throws AEADBadTagException if integrity tag is invalid
+ * @throws IllegalArgumentException if originalConnectionId is
+ * longer than 255 bytes
+ * @throws IllegalArgumentException if {@code version} isn't
+ * {@linkplain #getSupportedQuicVersions() supported}
+ */
+ void verifyRetryPacket(QuicVersion version, ByteBuffer originalConnectionId,
+ ByteBuffer packet) throws AEADBadTagException, QuicTransportException;
+
+ /**
+ * If the current handshake state is {@link HandshakeState#NEED_SEND_CRYPTO}
+ * meaning that a CRYPTO frame needs to be sent then this method is called
+ * to obtain the contents of the frame. Current handshake state
+ * can be obtained from {@link #getHandshakeState()}, and the current
+ * key space can be obtained with {@link #getCurrentSendKeySpace()}
+ * The bytes returned by this call are used to build a CRYPTO frame.
+ *
+ * @param keySpace the key space of the packet in which the
+ * requested data will be placed
+ * @return buffer containing data that will be put by caller in a CRYPTO
+ * frame, or null if there are no more handshake bytes to send in
+ * this key space at this time.
+ */
+ ByteBuffer getHandshakeBytes(KeySpace keySpace) throws IOException;
+
+ /**
+ * This method consumes crypto stream.
+ *
+ * @param keySpace the key space of the packet in which the provided
+ * crypto data was encountered.
+ * @param payload contents of the next CRYPTO frame
+ * @throws IllegalArgumentException if keySpace is ZERORTT or
+ * payload is empty
+ * @throws QuicTransportException if the handshake failed
+ */
+ void consumeHandshakeBytes(KeySpace keySpace, ByteBuffer payload)
+ throws QuicTransportException;
+
+ /**
+ * Returns a delegated {@code Runnable} task for
+ * this {@code QuicTLSEngine}.
+ *
+ * {@code QuicTLSEngine} operations may require the results of
+ * operations that block, or may take an extended period of time to
+ * complete. This method is used to obtain an outstanding {@link
+ * java.lang.Runnable} operation (task). Each task must be assigned
+ * a thread (possibly the current) to perform the {@link
+ * java.lang.Runnable#run() run} operation. Once the
+ * {@code run} method returns, the {@code Runnable} object
+ * is no longer needed and may be discarded.
+ *
+ * A call to this method will return each outstanding task
+ * exactly once.
+ *
+ * Multiple delegated tasks can be run in parallel.
+ *
+ * @return a delegated {@code Runnable} task, or null
+ * if none are available.
+ */
+ Runnable getDelegatedTask();
+
+ /**
+ * Called to check if a {@code HANDSHAKE_DONE} frame needs to be sent by the
+ * server. This method will only be called for a {@code QuicTLSEngine} which
+ * is in {@linkplain #getUseClientMode() server mode}. If the current TLS handshake
+ * state is
+ * {@link HandshakeState#NEED_SEND_HANDSHAKE_DONE
+ * NEED_SEND_HANDSHAKE_DONE} then this method returns {@code true} and
+ * advances the TLS handshake state to
+ * {@link HandshakeState#HANDSHAKE_CONFIRMED HANDSHAKE_CONFIRMED}. Else
+ * returns {@code false}.
+ *
+ * @return true if handshake state was {@code NEED_SEND_HANDSHAKE_DONE},
+ * false otherwise
+ * @throws IllegalStateException If this {@code QuicTLSEngine} is
+ * not in server mode
+ */
+ boolean tryMarkHandshakeDone() throws IllegalStateException;
+
+ /**
+ * Called when HANDSHAKE_DONE message is received from the server. This
+ * method will only be called for a {@code QuicTLSEngine} which is in
+ * {@linkplain #getUseClientMode() client mode}. If the current TLS handshake state
+ * is
+ * {@link HandshakeState#NEED_RECV_HANDSHAKE_DONE
+ * NEED_RECV_HANDSHAKE_DONE} then this method returns {@code true} and
+ * advances the TLS handshake state to
+ * {@link HandshakeState#HANDSHAKE_CONFIRMED HANDSHAKE_CONFIRMED}. Else
+ * returns {@code false}.
+ *
+ * @return true if handshake state was {@code NEED_RECV_HANDSHAKE_DONE},
+ * false otherwise
+ * @throws IllegalStateException if this {@code QuicTLSEngine} is
+ * not in client mode
+ */
+ boolean tryReceiveHandshakeDone() throws IllegalStateException;
+
+ /**
+ * Called when the client and the server, during the connection creation
+ * handshake, have settled on a Quic version to use for the connection. This
+ * can happen either due to an explicit version negotiation (as outlined in
+ * Quic RFC) or the server accepting the Quic version that the client chose
+ * in its first INITIAL packet. In either of those cases, this method will
+ * be called.
+ *
+ * @param quicVersion the negotiated {@code QuicVersion}
+ * @throws IllegalArgumentException if the {@code quicVersion} isn't
+ * {@linkplain #getSupportedQuicVersions() supported} on this engine
+ */
+ void versionNegotiated(QuicVersion quicVersion);
+
+ /**
+ * Sets the {@link QuicOneRttContext} on the {@code QuicTLSEngine}.
+ *
The {@code ctx} will be used by the {@code QuicTLSEngine} to access contextual 1-RTT
+ * data that might be required for the TLS operations.
+ *
+ * @param ctx the 1-RTT context to set
+ * @throws NullPointerException if {@code ctx} is null
+ */
+ void setOneRttContext(QuicOneRttContext ctx);
+}
diff --git a/src/java.base/share/classes/jdk/internal/net/quic/QuicTransportErrors.java b/src/java.base/share/classes/jdk/internal/net/quic/QuicTransportErrors.java
new file mode 100644
index 00000000000..d081458d40e
--- /dev/null
+++ b/src/java.base/share/classes/jdk/internal/net/quic/QuicTransportErrors.java
@@ -0,0 +1,349 @@
+/*
+ * Copyright (c) 2021, 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. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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.internal.net.quic;
+
+import sun.security.ssl.Alert;
+
+import java.util.Optional;
+import java.util.stream.Stream;
+
+/**
+ * An enum to model Quic transport errors.
+ * Some errors have a single possible code value, some, like
+ * {@link #CRYPTO_ERROR} have a range of possible values.
+ * Usually, the value (a long) would be used instead of the
+ * enum, but the enum itself can be useful - for instance in
+ * switch statements.
+ * This enum models QUIC transport error codes as defined in
+ * RFC 9000, section 20.1.
+ */
+public enum QuicTransportErrors {
+ /**
+ * No error.
+ *
+ * From
+ * RFC 9000, Section 20.1:
+ *
{@code
+ * An endpoint uses this with CONNECTION_CLOSE to signal that
+ * the connection is being closed abruptly in the absence
+ * of any error.
+ * }
+ */
+ NO_ERROR(0x00),
+
+ /**
+ * Internal Error.
+ *
+ * From
+ * RFC 9000, Section 20.1:
+ *
{@code
+ * The endpoint encountered an internal error and cannot
+ * continue with the connection.
+ * }
+ */
+ INTERNAL_ERROR(0x01),
+
+ /**
+ * Connection refused error.
+ *
+ * From
+ * RFC 9000, Section 20.1:
+ *
{@code
+ * The server refused to accept a new connection.
+ * }
+ */
+ CONNECTION_REFUSED(0x02),
+
+ /**
+ * Flow control error.
+ *
+ * From
+ * RFC 9000, Section 20.1:
+ *
{@code
+ * An endpoint received more data than it permitted in its advertised data limits;
+ * see Section 4.
+ * }
+ * @see
+ * RFC 9000, Section 20.1:
+ * {@code
+ * An endpoint received a frame for a stream identifier that exceeded its advertised
+ * stream limit for the corresponding stream type.
+ * }
+ */
+ STREAM_LIMIT_ERROR(0x04),
+
+ /**
+ * Stream state error.
+ *
+ * From
+ * RFC 9000, Section 20.1:
+ *
{@code
+ * An endpoint received a frame for a stream that was not in a state that permitted
+ * that frame; see Section 3.
+ * }
+ * @see
+ * RFC 9000, Section 20.1:
+ * {@code
+ * (1) An endpoint received a STREAM frame containing data that exceeded the previously
+ * established final size,
+ * (2) an endpoint received a STREAM frame or a RESET_STREAM frame containing a final
+ * size that was lower than the size of stream data that was already received, or
+ * (3) an endpoint received a STREAM frame or a RESET_STREAM frame containing a
+ * different final size to the one already established.
+ * }
+ */
+ FINAL_SIZE_ERROR(0x06),
+
+ /**
+ * Frame encoding error.
+ *
+ * From
+ * RFC 9000, Section 20.1:
+ *
{@code
+ * An endpoint received a frame that was badly formatted -- for instance,
+ * a frame of an unknown type or an ACK frame that has more
+ * acknowledgment ranges than the remainder of the packet could carry.
+ * }
+ */
+ FRAME_ENCODING_ERROR(0x07),
+
+ /**
+ * Transport parameter error.
+ *
+ * From
+ * RFC 9000, Section 20.1:
+ *
{@code
+ * An endpoint received transport parameters that were badly
+ * formatted, included an invalid value, omitted a mandatory
+ * transport parameter, included a forbidden transport
+ * parameter, or were otherwise in error.
+ * }
+ */
+ TRANSPORT_PARAMETER_ERROR(0x08),
+
+ /**
+ * Connection id limit error.
+ *
+ * From
+ * RFC 9000, Section 20.1:
+ *
{@code
+ * The number of connection IDs provided by the peer exceeds
+ * the advertised active_connection_id_limit.
+ * }
+ */
+ CONNECTION_ID_LIMIT_ERROR(0x09),
+
+ /**
+ * Protocol violiation error.
+ *
+ * From
+ * RFC 9000, Section 20.1:
+ *
{@code
+ * An endpoint detected an error with protocol compliance that
+ * was not covered by more specific error codes.
+ * }
+ */
+ PROTOCOL_VIOLATION(0x0a),
+
+ /**
+ * Invalid token error.
+ *
+ * From
+ * RFC 9000, Section 20.1:
+ *
{@code
+ * A server received a client Initial that contained an invalid Token field.
+ * }
+ */
+ INVALID_TOKEN(0x0b),
+
+ /**
+ * Application error.
+ *
+ * From
+ * RFC 9000, Section 20.1:
+ *
{@code
+ * The application or application protocol caused the connection to be closed.
+ * }
+ */
+ APPLICATION_ERROR(0x0c),
+
+ /**
+ * Crypto buffer exceeded error.
+ *
+ * From
+ * RFC 9000, Section 20.1:
+ *
{@code
+ * An endpoint has received more data in CRYPTO frames than it can buffer.
+ * }
+ */
+ CRYPTO_BUFFER_EXCEEDED(0x0d),
+
+ /**
+ * Key update error.
+ *
+ * From
+ * RFC 9000, Section 20.1:
+ *
{@code
+ * An endpoint detected errors in performing key updates; see Section 6 of [QUIC-TLS].
+ * }
+ * @see Section 6 of RFC 9001 [QUIC-TLS]
+ */
+ KEY_UPDATE_ERROR(0x0e),
+
+ /**
+ * AEAD limit reached error
+ *
+ * From
+ * RFC 9000, Section 20.1:
+ *
{@code
+ * An endpoint has reached the confidentiality or integrity limit
+ * for the AEAD algorithm used by the given connection.
+ * }
+ */
+ AEAD_LIMIT_REACHED(0x0f),
+
+ /**
+ * No viable path error.
+ *
+ * From
+ * RFC 9000, Section 20.1:
+ *
{@code
+ * An endpoint has determined that the network path is incapable of
+ * supporting QUIC. An endpoint is unlikely to receive a
+ * CONNECTION_CLOSE frame carrying this code except when the
+ * path does not support a large enough MTU.
+ * }
+ */
+ NO_VIABLE_PATH(0x10),
+
+ /**
+ * Error negotiating version.
+ * @spec https://www.rfc-editor.org/rfc/rfc9368#name-version-downgrade-preventio
+ * RFC 9368, Section 4
+ */
+ VERSION_NEGOTIATION_ERROR(0x11),
+
+ /**
+ * Crypto error.
+ *
+ * From
+ * RFC 9000, Section 20.1:
+ *
{@code
+ * The cryptographic handshake failed. A range of 256 values is
+ * reserved for carrying error codes specific to the cryptographic
+ * handshake that is used. Codes for errors occurring when
+ * TLS is used for the cryptographic handshake are described
+ * in Section 4.8 of [QUIC-TLS].
+ * }
+ * @see Section 4.8 of RFC 9001 [QUIC-TLS]
+ */
+ CRYPTO_ERROR(0x0100, 0x01ff);
+
+ private final long from;
+ private final long to;
+
+ QuicTransportErrors(long code) {
+ this(code, code);
+ }
+
+ QuicTransportErrors(long from, long to) {
+ assert from <= to;
+ this.from = from;
+ this.to = to;
+ }
+
+ /**
+ * {@return the code for this transport error, if this error
+ * {@linkplain #hasCode() has a single possible code value},
+ * {@code -1} otherwise}
+ */
+ public long code() { return hasCode() ? from : -1;}
+
+ /**
+ * {@return true if this error has a single possible code value}
+ */
+ public boolean hasCode() { return from == to; }
+
+ /**
+ * {@return true if this error has a range of possible code values}
+ */
+ public boolean hasRange() { return from < to;}
+
+ /**
+ * {@return the first possible code value in the range, or the
+ * code value if this error has a single possible code value}
+ */
+ public long from() {return from;}
+
+ /**
+ * {@return the last possible code value in the range, or the
+ * code value if this error has a single possible code value}
+ */
+ public long to() { return to; }
+
+ /**
+ * Tells whether the given {@code code} value corresponds to
+ * this error.
+ * @param code an error code value
+ * @return true if the given {@code code} value corresponds to
+ * this error.
+ */
+ boolean isFor(long code) {
+ return code >= from && code <= to;
+ }
+
+ /**
+ * {@return the {@link QuicTransportErrors} instance corresponding
+ * to the given {@code code} value, if any}
+ * @param code a {@code code} value
+ */
+ public static Optional ofCode(long code) {
+ return Stream.of(values()).filter(e -> e.isFor(code)).findAny();
+ }
+
+ public static String toString(long code) {
+ Optional c = Stream.of(values()).filter(e -> e.isFor(code)).findAny();
+ if (c.isEmpty()) return "Unknown [0x"+Long.toHexString(code) + "]";
+ if (c.get().hasCode()) return c.get().toString();
+ if (c.get() == CRYPTO_ERROR)
+ return c.get() + "|" + Alert.nameOf((byte)code);
+ return c.get() + " [0x" + Long.toHexString(code) + "]";
+
+ }
+}
diff --git a/src/java.base/share/classes/jdk/internal/net/quic/QuicTransportException.java b/src/java.base/share/classes/jdk/internal/net/quic/QuicTransportException.java
new file mode 100644
index 00000000000..3341cc527f2
--- /dev/null
+++ b/src/java.base/share/classes/jdk/internal/net/quic/QuicTransportException.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (c) 2022, 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
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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.internal.net.quic;
+
+/**
+ * Exception that wraps QUIC transport error codes.
+ * Thrown in response to packets or frames that violate QUIC protocol.
+ * This is a fatal exception; connection is always closed when this exception is caught.
+ *
+ * For a list of errors see:
+ * https://www.rfc-editor.org/rfc/rfc9000.html#name-transport-error-codes
+ */
+public final class QuicTransportException extends Exception {
+ @java.io.Serial
+ private static final long serialVersionUID = 5259674758792412464L;
+
+ private final QuicTLSEngine.KeySpace keySpace;
+ private final long frameType;
+ private final long errorCode;
+
+ /**
+ * Constructs a new {@code QuicTransportException}.
+ *
+ * @param reason the reason why the exception occurred
+ * @param keySpace the key space in which the frame appeared.
+ * May be {@code null}, for instance, in
+ * case of {@link QuicTransportErrors#INTERNAL_ERROR}.
+ * @param frameType the frame type of the frame whose parsing / handling
+ * caused the error.
+ * May be 0 if not related to any specific frame.
+ * @param errorCode a quic transport error
+ */
+ public QuicTransportException(String reason, QuicTLSEngine.KeySpace keySpace,
+ long frameType, QuicTransportErrors errorCode) {
+ super(reason);
+ this.keySpace = keySpace;
+ this.frameType = frameType;
+ this.errorCode = errorCode.code();
+ }
+
+ /**
+ * Constructs a new {@code QuicTransportException}. For use with TLS alerts.
+ *
+ * @param reason the reason why the exception occurred
+ * @param keySpace the key space in which the frame appeared.
+ * May be {@code null}, for instance, in
+ * case of {@link QuicTransportErrors#INTERNAL_ERROR}.
+ * @param frameType the frame type of the frame whose parsing / handling
+ * caused the error.
+ * May be 0 if not related to any specific frame.
+ * @param errorCode a quic transport error code
+ * @param cause the cause
+ */
+ public QuicTransportException(String reason, QuicTLSEngine.KeySpace keySpace,
+ long frameType, long errorCode, Throwable cause) {
+ super(reason, cause);
+ this.keySpace = keySpace;
+ this.frameType = frameType;
+ this.errorCode = errorCode;
+ }
+
+ /**
+ * {@return the reason to include in the {@code ConnectionCloseFrame}}
+ */
+ public String getReason() {
+ return getMessage();
+ }
+
+ /**
+ * {@return the key space for which the error occurred, or {@code null}}
+ */
+ public QuicTLSEngine.KeySpace getKeySpace() {
+ return keySpace;
+ }
+
+ /**
+ * {@return the frame type for which the error occurred, or 0}
+ */
+ public long getFrameType() {
+ return frameType;
+ }
+
+ /**
+ * {@return the transport error that occurred}
+ */
+ public long getErrorCode() {
+ return errorCode;
+ }
+}
diff --git a/src/java.base/share/classes/jdk/internal/net/quic/QuicTransportParametersConsumer.java b/src/java.base/share/classes/jdk/internal/net/quic/QuicTransportParametersConsumer.java
new file mode 100644
index 00000000000..9429f6bf26f
--- /dev/null
+++ b/src/java.base/share/classes/jdk/internal/net/quic/QuicTransportParametersConsumer.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2022, 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. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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.internal.net.quic;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Interface for consumer of QUIC transport parameters, in wire-encoded format
+ */
+public interface QuicTransportParametersConsumer {
+ /**
+ * Consumes the provided QUIC transport parameters
+ * @param buffer byte buffer containing encoded quic transport parameters
+ * @throws QuicTransportException if buffer does not represent valid parameters
+ */
+ void accept(ByteBuffer buffer) throws QuicTransportException;
+}
diff --git a/src/java.base/share/classes/jdk/internal/net/quic/QuicVersion.java b/src/java.base/share/classes/jdk/internal/net/quic/QuicVersion.java
new file mode 100644
index 00000000000..14bbaba816d
--- /dev/null
+++ b/src/java.base/share/classes/jdk/internal/net/quic/QuicVersion.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (c) 2022, 2025, 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. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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.internal.net.quic;
+
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * Represents the Quic versions defined in their corresponding RFCs
+ */
+public enum QuicVersion {
+ // the version numbers are defined in their respective RFCs
+ QUIC_V1(1), // RFC-9000
+ QUIC_V2(0x6b3343cf); // RFC 9369
+
+ // 32 bits unsigned integer representing the version as
+ // defined in RFC. This is the version number as sent
+ // in long headers packets (see RFC 9000).
+ private final int versionNumber;
+
+ private QuicVersion(final int versionNumber) {
+ this.versionNumber = versionNumber;
+ }
+
+ /**
+ * {@return the version number}
+ */
+ public int versionNumber() {
+ return this.versionNumber;
+ }
+
+ /**
+ * {@return the QuicVersion corresponding to the {@code versionNumber} or
+ * {@link Optional#empty() an empty Optional} if the {@code versionNumber}
+ * doesn't correspond to a Quic version}
+ *
+ * @param versionNumber The version number
+ */
+ public static Optional of(int versionNumber) {
+ for (QuicVersion qv : QuicVersion.values()) {
+ if (qv.versionNumber == versionNumber) {
+ return Optional.of(qv);
+ }
+ }
+ return Optional.empty();
+ }
+
+ /**
+ * From among the {@code quicVersions}, selects a {@code QuicVersion} to be used in the
+ * first packet during connection initiation.
+ *
+ * @param quicVersions the available QUIC versions
+ * @return the QUIC version to use in the first packet
+ * @throws NullPointerException if {@code quicVersions} is null or any element
+ * in it is null
+ * @throws IllegalArgumentException if {@code quicVersions} is empty
+ */
+ public static QuicVersion firstFlightVersion(final Collection quicVersions) {
+ Objects.requireNonNull(quicVersions);
+ if (quicVersions.isEmpty()) {
+ throw new IllegalArgumentException("Empty quic versions");
+ }
+ if (quicVersions.size() == 1) {
+ return quicVersions.iterator().next();
+ }
+ for (final QuicVersion version : quicVersions) {
+ if (version == QUIC_V1) {
+ // we always prefer QUIC v1 for first flight version
+ return QUIC_V1;
+ }
+ }
+ // the given versions did not have QUIC v1, which implies the
+ // only available first flight version is QUIC v2
+ return QUIC_V2;
+ }
+}
diff --git a/src/java.base/share/classes/module-info.java b/src/java.base/share/classes/module-info.java
index 2a51a0af38d..3ae84fdf198 100644
--- a/src/java.base/share/classes/module-info.java
+++ b/src/java.base/share/classes/module-info.java
@@ -190,6 +190,8 @@ module java.base {
jdk.jlink;
exports jdk.internal.logger to
java.logging;
+ exports jdk.internal.net.quic to
+ java.net.http;
exports jdk.internal.org.xml.sax to
jdk.jfr;
exports jdk.internal.org.xml.sax.helpers to
@@ -260,6 +262,7 @@ module java.base {
jdk.jfr;
exports jdk.internal.util to
java.desktop,
+ java.net.http,
java.prefs,
java.security.jgss,
java.smartcardio,
diff --git a/src/java.base/share/classes/sun/security/ssl/Alert.java b/src/java.base/share/classes/sun/security/ssl/Alert.java
index 4e1ccf385c7..960b3f3b37d 100644
--- a/src/java.base/share/classes/sun/security/ssl/Alert.java
+++ b/src/java.base/share/classes/sun/security/ssl/Alert.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2003, 2024, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2003, 2025, 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
@@ -34,9 +34,9 @@ import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLProtocolException;
/**
- * SSL/(D)TLS Alter description
+ * SSL/(D)TLS Alert description
*/
-enum Alert {
+public enum Alert {
// Please refer to TLS Alert Registry for the latest (D)TLS Alert values:
// https://www.iana.org/assignments/tls-parameters/
CLOSE_NOTIFY ((byte)0, "close_notify", false),
@@ -103,7 +103,7 @@ enum Alert {
return null;
}
- static String nameOf(byte id) {
+ public static String nameOf(byte id) {
for (Alert al : Alert.values()) {
if (al.id == id) {
return al.description;
diff --git a/src/java.base/share/classes/sun/security/ssl/AlpnExtension.java b/src/java.base/share/classes/sun/security/ssl/AlpnExtension.java
index d44ec034411..aa5933ddab0 100644
--- a/src/java.base/share/classes/sun/security/ssl/AlpnExtension.java
+++ b/src/java.base/share/classes/sun/security/ssl/AlpnExtension.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2025, 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
@@ -71,7 +71,7 @@ final class AlpnExtension {
/**
* The "application_layer_protocol_negotiation" extension.
- *
+ *
* See RFC 7301 for the specification of this extension.
*/
static final class AlpnSpec implements SSLExtensionSpec {
@@ -344,6 +344,13 @@ final class AlpnExtension {
// The producing happens in server side only.
ServerHandshakeContext shc = (ServerHandshakeContext)context;
+ if (shc.sslConfig.isQuic) {
+ // RFC 9001: endpoints MUST use ALPN
+ throw shc.conContext.fatal(
+ Alert.NO_APPLICATION_PROTOCOL,
+ "Client did not offer application layer protocol");
+ }
+
// Please don't use the previous negotiated application protocol.
shc.applicationProtocol = "";
shc.conContext.applicationProtocol = "";
@@ -513,6 +520,15 @@ final class AlpnExtension {
// The producing happens in client side only.
ClientHandshakeContext chc = (ClientHandshakeContext)context;
+ if (chc.sslConfig.isQuic) {
+ // RFC 9001: QUIC clients MUST use error 0x0178
+ // [no_application_protocol] to terminate a connection when
+ // ALPN negotiation fails
+ throw chc.conContext.fatal(
+ Alert.NO_APPLICATION_PROTOCOL,
+ "Server did not offer application layer protocol");
+ }
+
// Please don't use the previous negotiated application protocol.
chc.applicationProtocol = "";
chc.conContext.applicationProtocol = "";
diff --git a/src/java.base/share/classes/sun/security/ssl/CertificateMessage.java b/src/java.base/share/classes/sun/security/ssl/CertificateMessage.java
index 609a81571ed..d4587d35ae9 100644
--- a/src/java.base/share/classes/sun/security/ssl/CertificateMessage.java
+++ b/src/java.base/share/classes/sun/security/ssl/CertificateMessage.java
@@ -1219,12 +1219,19 @@ final class CertificateMessage {
certs.clone(),
authType,
engine);
- } else {
- SSLSocket socket = (SSLSocket)shc.conContext.transport;
+ } else if (shc.conContext.transport instanceof SSLSocket socket){
((X509ExtendedTrustManager)tm).checkClientTrusted(
certs.clone(),
authType,
socket);
+ } else if (shc.conContext.transport
+ instanceof QuicTLSEngineImpl qtlse) {
+ if (tm instanceof X509TrustManagerImpl tmImpl) {
+ tmImpl.checkClientTrusted(certs.clone(), authType, qtlse);
+ } else {
+ throw new CertificateException(
+ "QUIC only supports SunJSSE trust managers");
+ }
}
} else {
// Unlikely to happen, because we have wrapped the old
@@ -1268,18 +1275,26 @@ final class CertificateMessage {
try {
X509TrustManager tm = chc.sslContext.getX509TrustManager();
- if (tm instanceof X509ExtendedTrustManager) {
+ if (tm instanceof X509ExtendedTrustManager x509ExtTm) {
if (chc.conContext.transport instanceof SSLEngine engine) {
- ((X509ExtendedTrustManager)tm).checkServerTrusted(
+ x509ExtTm.checkServerTrusted(
certs.clone(),
authType,
engine);
- } else {
- SSLSocket socket = (SSLSocket)chc.conContext.transport;
- ((X509ExtendedTrustManager)tm).checkServerTrusted(
+ } else if (chc.conContext.transport instanceof SSLSocket socket) {
+ x509ExtTm.checkServerTrusted(
certs.clone(),
authType,
socket);
+ } else if (chc.conContext.transport instanceof QuicTLSEngineImpl qtlse) {
+ if (x509ExtTm instanceof X509TrustManagerImpl tmImpl) {
+ tmImpl.checkServerTrusted(certs.clone(), authType, qtlse);
+ } else {
+ throw new CertificateException(
+ "QUIC only supports SunJSSE trust managers");
+ }
+ } else {
+ throw new AssertionError("Unexpected transport type");
}
} else {
// Unlikely to happen, because we have wrapped the old
diff --git a/src/java.base/share/classes/sun/security/ssl/ClientHello.java b/src/java.base/share/classes/sun/security/ssl/ClientHello.java
index 3e43921520d..c9432ea3979 100644
--- a/src/java.base/share/classes/sun/security/ssl/ClientHello.java
+++ b/src/java.base/share/classes/sun/security/ssl/ClientHello.java
@@ -568,7 +568,7 @@ final class ClientHello {
}
if (sessionId.length() == 0 &&
chc.maximumActiveProtocol.useTLS13PlusSpec() &&
- SSLConfiguration.useCompatibilityMode) {
+ chc.sslConfig.isUseCompatibilityMode()) {
// In compatibility mode, the TLS 1.3 legacy_session_id
// field MUST be non-empty, so a client not offering a
// pre-TLS 1.3 session MUST generate a new 32-byte value.
diff --git a/src/java.base/share/classes/sun/security/ssl/Finished.java b/src/java.base/share/classes/sun/security/ssl/Finished.java
index 9421d12ec15..04fe61760d0 100644
--- a/src/java.base/share/classes/sun/security/ssl/Finished.java
+++ b/src/java.base/share/classes/sun/security/ssl/Finished.java
@@ -846,6 +846,16 @@ final class Finished {
// update the context for the following key derivation
shc.handshakeKeyDerivation = secretKD;
+ if (shc.sslConfig.isQuic) {
+ QuicTLSEngineImpl engine =
+ (QuicTLSEngineImpl) shc.conContext.transport;
+ try {
+ engine.deriveOneRTTKeys();
+ } catch (IOException e) {
+ throw shc.conContext.fatal(Alert.INTERNAL_ERROR,
+ "Failure to derive application secrets", e);
+ }
+ }
} catch (GeneralSecurityException gse) {
throw shc.conContext.fatal(Alert.INTERNAL_ERROR,
"Failure to derive application secrets", gse);
@@ -1010,6 +1020,16 @@ final class Finished {
// update the context for the following key derivation
chc.handshakeKeyDerivation = secretKD;
+ if (chc.sslConfig.isQuic) {
+ QuicTLSEngineImpl engine =
+ (QuicTLSEngineImpl) chc.conContext.transport;
+ try {
+ engine.deriveOneRTTKeys();
+ } catch (IOException e) {
+ throw chc.conContext.fatal(Alert.INTERNAL_ERROR,
+ "Failure to derive application secrets", e);
+ }
+ }
} catch (GeneralSecurityException gse) {
throw chc.conContext.fatal(Alert.INTERNAL_ERROR,
"Failure to derive application secrets", gse);
diff --git a/src/java.base/share/classes/sun/security/ssl/KeyUpdate.java b/src/java.base/share/classes/sun/security/ssl/KeyUpdate.java
index c4549070f02..2b17c7406a3 100644
--- a/src/java.base/share/classes/sun/security/ssl/KeyUpdate.java
+++ b/src/java.base/share/classes/sun/security/ssl/KeyUpdate.java
@@ -269,6 +269,12 @@ final class KeyUpdate {
HandshakeMessage message) throws IOException {
// The producing happens in server side only.
PostHandshakeContext hc = (PostHandshakeContext)context;
+ if (hc.sslConfig.isQuic) {
+ // Quic doesn't allow KEY_UPDATE TLS message. It has its own Quic specific
+ // key update mechanism, RFC-9001, section 6:
+ // Endpoints MUST NOT send a TLS KeyUpdate message.
+ return null;
+ }
KeyUpdateMessage km = (KeyUpdateMessage)message;
if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
SSLLogger.fine(
diff --git a/src/java.base/share/classes/sun/security/ssl/OutputRecord.java b/src/java.base/share/classes/sun/security/ssl/OutputRecord.java
index 0fa831f6351..f2c30b3ff72 100644
--- a/src/java.base/share/classes/sun/security/ssl/OutputRecord.java
+++ b/src/java.base/share/classes/sun/security/ssl/OutputRecord.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 1996, 2022, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1996, 2025, 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
@@ -31,6 +31,8 @@ import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.concurrent.locks.ReentrantLock;
+
+import jdk.internal.net.quic.QuicTLSEngine;
import sun.security.ssl.SSLCipher.SSLWriteCipher;
/**
@@ -154,6 +156,16 @@ abstract class OutputRecord
throw new UnsupportedOperationException();
}
+ // apply to QuicEngine only
+ byte[] getHandshakeMessage() {
+ throw new UnsupportedOperationException();
+ }
+
+ // apply to QuicEngine only
+ QuicTLSEngine.KeySpace getHandshakeMessageKeySpace() {
+ throw new UnsupportedOperationException();
+ }
+
// apply to SSLEngine only
void encodeV2NoCipher() throws IOException {
throw new UnsupportedOperationException();
diff --git a/src/java.base/share/classes/sun/security/ssl/PostHandshakeContext.java b/src/java.base/share/classes/sun/security/ssl/PostHandshakeContext.java
index b06549b40e3..a4f87616245 100644
--- a/src/java.base/share/classes/sun/security/ssl/PostHandshakeContext.java
+++ b/src/java.base/share/classes/sun/security/ssl/PostHandshakeContext.java
@@ -47,17 +47,15 @@ final class PostHandshakeContext extends HandshakeContext {
context.conSession.getLocalSupportedSignatureSchemes());
// Add the potential post-handshake consumers.
- if (context.sslConfig.isClientMode) {
+ if (!context.sslConfig.isQuic) {
handshakeConsumers.putIfAbsent(
SSLHandshake.KEY_UPDATE.id,
SSLHandshake.KEY_UPDATE);
+ }
+ if (context.sslConfig.isClientMode) {
handshakeConsumers.putIfAbsent(
SSLHandshake.NEW_SESSION_TICKET.id,
SSLHandshake.NEW_SESSION_TICKET);
- } else {
- handshakeConsumers.putIfAbsent(
- SSLHandshake.KEY_UPDATE.id,
- SSLHandshake.KEY_UPDATE);
}
handshakeFinished = true;
@@ -93,6 +91,15 @@ final class PostHandshakeContext extends HandshakeContext {
static boolean isConsumable(TransportContext context, byte handshakeType) {
if (handshakeType == SSLHandshake.KEY_UPDATE.id) {
+ // Quic doesn't allow KEY_UPDATE TLS message. It has its own
+ // Quic-specific key update mechanism, RFC-9001, section 6:
+ // Endpoints MUST NOT send a TLS KeyUpdate message. Endpoints
+ // MUST treat the receipt of a TLS KeyUpdate message as a
+ // connection error of type 0x010a, equivalent to a fatal
+ // TLS alert of unexpected_message;
+ if (context.sslConfig.isQuic) {
+ return false;
+ }
// The KeyUpdate handshake message does not apply to TLS 1.2 and
// previous protocols.
return context.protocolVersion.useTLS13PlusSpec();
diff --git a/src/java.base/share/classes/sun/security/ssl/QuicCipher.java b/src/java.base/share/classes/sun/security/ssl/QuicCipher.java
new file mode 100644
index 00000000000..c1d812e4c40
--- /dev/null
+++ b/src/java.base/share/classes/sun/security/ssl/QuicCipher.java
@@ -0,0 +1,699 @@
+/*
+ * Copyright (c) 2022, 2025, 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. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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 sun.security.ssl;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.security.GeneralSecurityException;
+import java.security.Security;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicLong;
+
+import javax.crypto.AEADBadTagException;
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+import javax.crypto.ShortBufferException;
+import javax.crypto.spec.ChaCha20ParameterSpec;
+import javax.crypto.spec.GCMParameterSpec;
+import javax.crypto.spec.IvParameterSpec;
+
+import jdk.internal.net.quic.QuicTransportErrors;
+import jdk.internal.net.quic.QuicTransportException;
+import sun.security.util.KeyUtil;
+
+import static jdk.internal.net.quic.QuicTLSEngine.KeySpace.ONE_RTT;
+import static sun.security.ssl.QuicTLSEngineImpl.BASE_CRYPTO_ERROR;
+
+abstract class QuicCipher {
+ private static final String
+ SEC_PROP_QUIC_TLS_KEY_LIMITS = "jdk.quic.tls.keyLimits";
+
+ private static final Map KEY_LIMITS;
+
+ static {
+ final String propVal = Security.getProperty(
+ SEC_PROP_QUIC_TLS_KEY_LIMITS);
+ if (propVal == null) {
+ KEY_LIMITS = Map.of(); // no specific limits
+ } else {
+ final Map limits = new HashMap<>();
+ for (final String entry : propVal.split(",")) {
+ // each entry is of the form
+ // example:
+ // AES/GCM/NoPadding 2^23
+ // ChaCha20-Poly1305 -1
+ final String[] parts = entry.trim().split(" ");
+ if (parts.length != 2) {
+ // TODO: exception type
+ throw new RuntimeException("invalid value for "
+ + SEC_PROP_QUIC_TLS_KEY_LIMITS
+ + " security property");
+ }
+ final String cipher = parts[0];
+ if (limits.containsKey(cipher)) {
+ throw new RuntimeException(
+ "key limit defined more than once for cipher "
+ + cipher);
+ }
+ final String limitVal = parts[1];
+ final long limit;
+ final int index = limitVal.indexOf("^");
+ if (index >= 0) {
+ // of the form x^y (example: 2^23)
+ limit = (long) Math.pow(
+ Integer.parseInt(limitVal.substring(0, index)),
+ Integer.parseInt(limitVal.substring(index + 1)));
+ } else {
+ limit = Long.parseLong(limitVal);
+ }
+ if (limit == 0 || limit < -1) {
+ // we allow -1 to imply no limits, but any other zero
+ // or negative value is invalid
+ // TODO: exception type
+ throw new RuntimeException("invalid value for "
+ + SEC_PROP_QUIC_TLS_KEY_LIMITS
+ + " security property");
+ }
+ limits.put(cipher, limit);
+ }
+ KEY_LIMITS = Collections.unmodifiableMap(limits);
+ }
+ }
+
+ private final CipherSuite cipherSuite;
+ private final QuicHeaderProtectionCipher hpCipher;
+ private final SecretKey baseSecret;
+ private final int keyPhase;
+
+ protected QuicCipher(final CipherSuite cipherSuite, final SecretKey baseSecret,
+ final QuicHeaderProtectionCipher hpCipher, final int keyPhase) {
+ assert keyPhase == 0 || keyPhase == 1 :
+ "invalid key phase: " + keyPhase;
+ this.cipherSuite = cipherSuite;
+ this.baseSecret = baseSecret;
+ this.hpCipher = hpCipher;
+ this.keyPhase = keyPhase;
+ }
+
+ final SecretKey getBaseSecret() {
+ return this.baseSecret;
+ }
+
+ final CipherSuite getCipherSuite() {
+ return this.cipherSuite;
+ }
+
+ final SecretKey getHeaderProtectionKey() {
+ return this.hpCipher.headerProtectionKey;
+ }
+
+ final ByteBuffer computeHeaderProtectionMask(ByteBuffer sample)
+ throws QuicTransportException {
+ return hpCipher.computeHeaderProtectionMask(sample);
+ }
+
+ final int getKeyPhase() {
+ return this.keyPhase;
+ }
+
+ final void discard(boolean destroyHP) {
+ safeDiscard(this.baseSecret);
+ if (destroyHP) {
+ this.hpCipher.discard();
+ }
+ this.doDiscard();
+ }
+
+ protected abstract void doDiscard();
+
+ static QuicReadCipher createReadCipher(final CipherSuite cipherSuite,
+ final SecretKey baseSecret, final SecretKey key,
+ final byte[] iv, final SecretKey hp,
+ final int keyPhase) throws GeneralSecurityException {
+ return switch (cipherSuite) {
+ case TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384 ->
+ new T13GCMReadCipher(
+ cipherSuite, baseSecret, key, iv, hp, keyPhase);
+ case TLS_CHACHA20_POLY1305_SHA256 ->
+ new T13CC20P1305ReadCipher(
+ cipherSuite, baseSecret, key, iv, hp, keyPhase);
+ default -> throw new IllegalArgumentException("Cipher suite "
+ + cipherSuite + " not supported");
+ };
+ }
+
+ static QuicWriteCipher createWriteCipher(final CipherSuite cipherSuite,
+ final SecretKey baseSecret, final SecretKey key,
+ final byte[] iv, final SecretKey hp,
+ final int keyPhase) throws GeneralSecurityException {
+ return switch (cipherSuite) {
+ case TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384 ->
+ new T13GCMWriteCipher(cipherSuite, baseSecret, key, iv, hp,
+ keyPhase);
+ case TLS_CHACHA20_POLY1305_SHA256 ->
+ new T13CC20P1305WriteCipher(cipherSuite, baseSecret, key, iv,
+ hp, keyPhase);
+ default -> throw new IllegalArgumentException("Cipher suite "
+ + cipherSuite + " not supported");
+ };
+ }
+
+ static void safeDiscard(final SecretKey secretKey) {
+ KeyUtil.destroySecretKeys(secretKey);
+ }
+
+ abstract static class QuicReadCipher extends QuicCipher {
+ private final AtomicLong lowestDecryptedPktNum = new AtomicLong(-1);
+
+ QuicReadCipher(CipherSuite cipherSuite, SecretKey baseSecret,
+ QuicHeaderProtectionCipher hpCipher, int keyPhase) {
+ super(cipherSuite, baseSecret, hpCipher, keyPhase);
+ }
+
+ final void decryptPacket(long packetNumber, ByteBuffer packet,
+ int headerLength, ByteBuffer output)
+ throws AEADBadTagException, ShortBufferException, QuicTransportException {
+ doDecrypt(packetNumber, packet, headerLength, output);
+ boolean updated;
+ do {
+ final long current = lowestDecryptedPktNum.get();
+ assert packetNumber >= 0 :
+ "unexpected packet number: " + packetNumber;
+ final long newLowest = current == -1 ? packetNumber :
+ Math.min(current, packetNumber);
+ updated = lowestDecryptedPktNum.compareAndSet(current,
+ newLowest);
+ } while (!updated);
+ }
+
+ protected abstract void doDecrypt(long packetNumber,
+ ByteBuffer packet, int headerLength, ByteBuffer output)
+ throws AEADBadTagException, ShortBufferException, QuicTransportException;
+
+ /**
+ * Returns the maximum limit on the number of packets that fail
+ * decryption, across all key (updates), using this
+ * {@code QuicReadCipher}. This method must not return a value less
+ * than 0.
+ *
+ * @return the limit
+ */
+ // RFC-9001, section 6.6
+ abstract long integrityLimit();
+
+ /**
+ * {@return the lowest packet number that this {@code QuicReadCipher}
+ * has decrypted. If no packets have yet been decrypted by this
+ * instance, then this method returns -1}
+ */
+ final long lowestDecryptedPktNum() {
+ return this.lowestDecryptedPktNum.get();
+ }
+
+ /**
+ * {@return true if this {@code QuicReadCipher} has successfully
+ * decrypted any packet sent by the peer, else returns false}
+ */
+ final boolean hasDecryptedAny() {
+ return this.lowestDecryptedPktNum.get() != -1;
+ }
+ }
+
+ abstract static class QuicWriteCipher extends QuicCipher {
+ private final AtomicLong numPacketsEncrypted = new AtomicLong();
+ private final AtomicLong lowestEncryptedPktNum = new AtomicLong(-1);
+
+ QuicWriteCipher(CipherSuite cipherSuite, SecretKey baseSecret,
+ QuicHeaderProtectionCipher hpCipher, int keyPhase) {
+ super(cipherSuite, baseSecret, hpCipher, keyPhase);
+ }
+
+ final void encryptPacket(final long packetNumber,
+ final ByteBuffer packetHeader,
+ final ByteBuffer packetPayload,
+ final ByteBuffer output)
+ throws QuicTransportException, ShortBufferException {
+ final long confidentialityLimit = confidentialityLimit();
+ final long numEncrypted = this.numPacketsEncrypted.get();
+ if (confidentialityLimit > 0 &&
+ numEncrypted > confidentialityLimit) {
+ // the OneRttKeyManager is responsible for detecting and
+ // initiating a key update before this limit is hit. The fact
+ // that we hit this limit indicates that either the key
+ // update wasn't initiated or the key update failed. In
+ // either case we just throw an exception which
+ // should lead to the connection being closed as required by
+ // RFC-9001, section 6.6:
+ // If a key update is not possible or integrity limits are
+ // reached, the endpoint MUST stop using the connection and
+ // only send stateless resets in response to receiving
+ // packets. It is RECOMMENDED that endpoints immediately
+ // close the connection with a connection error of type
+ // AEAD_LIMIT_REACHED before reaching a state where key
+ // updates are not possible.
+ throw new QuicTransportException("confidentiality limit " +
+ "reached", ONE_RTT, 0,
+ QuicTransportErrors.AEAD_LIMIT_REACHED);
+ }
+ this.numPacketsEncrypted.incrementAndGet();
+ doEncryptPacket(packetNumber, packetHeader, packetPayload, output);
+ boolean updated;
+ do {
+ final long current = lowestEncryptedPktNum.get();
+ assert packetNumber >= 0 :
+ "unexpected packet number: " + packetNumber;
+ final long newLowest = current == -1 ? packetNumber :
+ Math.min(current, packetNumber);
+ updated = lowestEncryptedPktNum.compareAndSet(current,
+ newLowest);
+ } while (!updated);
+ }
+
+ /**
+ * {@return the lowest packet number that this {@code QuicWriteCipher}
+ * has encrypted. If no packets have yet been encrypted by this
+ * instance, then this method returns -1}
+ */
+ final long lowestEncryptedPktNum() {
+ return this.lowestEncryptedPktNum.get();
+ }
+
+ /**
+ * {@return true if this {@code QuicWriteCipher} has successfully
+ * encrypted any packet to send to the peer, else returns false}
+ */
+ final boolean hasEncryptedAny() {
+ // rely on the lowestEncryptedPktNum field instead of the
+ // numPacketsEncrypted field. this avoids a race where the
+ // lowestEncryptedPktNum() might return a value contradicting
+ // the return value of this method.
+ return this.lowestEncryptedPktNum.get() != -1;
+ }
+
+ /**
+ * {@return the number of packets encrypted by this {@code
+ * QuicWriteCipher}}
+ */
+ final long getNumEncrypted() {
+ return this.numPacketsEncrypted.get();
+ }
+
+ abstract void doEncryptPacket(long packetNumber, ByteBuffer packetHeader,
+ ByteBuffer packetPayload, ByteBuffer output)
+ throws ShortBufferException, QuicTransportException;
+
+ /**
+ * Returns the maximum limit on the number of packets that are allowed
+ * to be encrypted with this instance of {@code QuicWriteCipher}. A
+ * value less than 0 implies that there's no limit.
+ *
+ * @return the limit or -1
+ */
+ // RFC-9001, section 6.6: The confidentiality limit applies to the
+ // number of
+ // packets encrypted with a given key.
+ abstract long confidentialityLimit();
+ }
+
+ abstract static class QuicHeaderProtectionCipher {
+ protected final SecretKey headerProtectionKey;
+
+ protected QuicHeaderProtectionCipher(
+ final SecretKey headerProtectionKey) {
+ this.headerProtectionKey = headerProtectionKey;
+ }
+
+ int getHeaderProtectionSampleSize() {
+ return 16;
+ }
+
+ abstract ByteBuffer computeHeaderProtectionMask(ByteBuffer sample)
+ throws QuicTransportException;
+
+ final void discard() {
+ safeDiscard(this.headerProtectionKey);
+ }
+ }
+
+ static final class T13GCMReadCipher extends QuicReadCipher {
+ // RFC-9001, section 6.6: For AEAD_AES_128_GCM and AEAD_AES_256_GCM,
+ // the integrity limit is 2^52 invalid packets
+ private static final long INTEGRITY_LIMIT = 1L << 52;
+
+ private final Cipher cipher;
+ private final SecretKey key;
+ private final byte[] iv;
+
+ T13GCMReadCipher(final CipherSuite cipherSuite, final SecretKey baseSecret,
+ final SecretKey key, final byte[] iv, final SecretKey hp,
+ final int keyPhase)
+ throws GeneralSecurityException {
+ super(cipherSuite, baseSecret, new T13AESHPCipher(hp), keyPhase);
+ this.key = key;
+ this.iv = iv;
+ this.cipher = Cipher.getInstance("AES/GCM/NoPadding");
+ }
+
+ @Override
+ protected void doDecrypt(long packetNumber, ByteBuffer packet,
+ int headerLength, ByteBuffer output)
+ throws AEADBadTagException, ShortBufferException, QuicTransportException {
+ byte[] iv = this.iv.clone();
+
+ // apply packet number to IV
+ int i = 11;
+ while (packetNumber > 0) {
+ iv[i] ^= (byte) (packetNumber & 0xFF);
+ packetNumber = packetNumber >>> 8;
+ i--;
+ }
+ final GCMParameterSpec ivSpec = new GCMParameterSpec(128, iv);
+ synchronized (cipher) {
+ try {
+ cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);
+ int limit = packet.limit();
+ packet.limit(packet.position() + headerLength);
+ cipher.updateAAD(packet);
+ packet.limit(limit);
+ cipher.doFinal(packet, output);
+ } catch (AEADBadTagException | ShortBufferException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new QuicTransportException("Decryption failed",
+ null, 0, BASE_CRYPTO_ERROR + Alert.INTERNAL_ERROR.id, e);
+ }
+ }
+ }
+
+ @Override
+ long integrityLimit() {
+ return INTEGRITY_LIMIT;
+ }
+
+ @Override
+ protected final void doDiscard() {
+ safeDiscard(this.key);
+ }
+ }
+
+ static final class T13GCMWriteCipher extends QuicWriteCipher {
+ private static final String CIPHER_ALGORITHM_NAME = "AES/GCM/NoPadding";
+ private static final long CONFIDENTIALITY_LIMIT;
+
+ static {
+ // RFC-9001, section 6.6: For AEAD_AES_128_GCM and AEAD_AES_256_GCM,
+ // the confidentiality limit is 2^23 encrypted packets
+ final long defaultVal = 1 << 23;
+ long limit =
+ KEY_LIMITS.getOrDefault(CIPHER_ALGORITHM_NAME, defaultVal);
+ // don't allow the configuration to increase the confidentiality
+ // limit, but only let it lower the limit
+ limit = limit > defaultVal ? defaultVal : limit;
+ CONFIDENTIALITY_LIMIT = limit;
+ }
+
+ private final SecretKey key;
+ private final Cipher cipher;
+ private final byte[] iv;
+
+ T13GCMWriteCipher(final CipherSuite cipherSuite, final SecretKey baseSecret,
+ final SecretKey key, final byte[] iv, final SecretKey hp,
+ final int keyPhase) throws GeneralSecurityException {
+ super(cipherSuite, baseSecret, new T13AESHPCipher(hp), keyPhase);
+ this.key = key;
+ this.iv = iv;
+ this.cipher = Cipher.getInstance(CIPHER_ALGORITHM_NAME);
+ }
+
+ @Override
+ void doEncryptPacket(long packetNumber, ByteBuffer packetHeader,
+ ByteBuffer packetPayload, ByteBuffer output)
+ throws ShortBufferException, QuicTransportException {
+ byte[] iv = this.iv.clone();
+
+ // apply packet number to IV
+ int i = 11;
+ while (packetNumber > 0) {
+ iv[i] ^= (byte) (packetNumber & 0xFF);
+ packetNumber = packetNumber >>> 8;
+ i--;
+ }
+ final GCMParameterSpec ivSpec = new GCMParameterSpec(128, iv);
+ synchronized (cipher) {
+ try {
+ cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);
+ cipher.updateAAD(packetHeader);
+ cipher.doFinal(packetPayload, output);
+ } catch (ShortBufferException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new QuicTransportException("Encryption failed",
+ null, 0, BASE_CRYPTO_ERROR + Alert.INTERNAL_ERROR.id, e);
+ }
+ }
+ }
+
+ @Override
+ long confidentialityLimit() {
+ return CONFIDENTIALITY_LIMIT;
+ }
+
+ @Override
+ protected final void doDiscard() {
+ safeDiscard(this.key);
+ }
+ }
+
+ static final class T13AESHPCipher extends QuicHeaderProtectionCipher {
+ private final Cipher cipher;
+
+ T13AESHPCipher(SecretKey hp) throws GeneralSecurityException {
+ super(hp);
+ cipher = Cipher.getInstance("AES/ECB/NoPadding");
+ }
+
+ @Override
+ public ByteBuffer computeHeaderProtectionMask(ByteBuffer sample)
+ throws QuicTransportException {
+ if (sample.remaining() != getHeaderProtectionSampleSize()) {
+ throw new IllegalArgumentException("Invalid sample size");
+ }
+ ByteBuffer output = ByteBuffer.allocate(sample.remaining());
+ try {
+ synchronized (cipher) {
+ // Some providers (Jipher) don't re-initialize the cipher
+ // after doFinal, and need init every time.
+ cipher.init(Cipher.ENCRYPT_MODE, headerProtectionKey);
+ cipher.doFinal(sample, output);
+ }
+ output.flip();
+ assert output.remaining() >= 5;
+ return output;
+ } catch (Exception e) {
+ throw new QuicTransportException("Encryption failed",
+ null, 0, BASE_CRYPTO_ERROR + Alert.INTERNAL_ERROR.id, e);
+ }
+ }
+ }
+
+ static final class T13CC20P1305ReadCipher extends QuicReadCipher {
+ // RFC-9001, section 6.6: For AEAD_CHACHA20_POLY1305,
+ // the integrity limit is 2^36 invalid packets
+ private static final long INTEGRITY_LIMIT = 1L << 36;
+
+ private final SecretKey key;
+ private final Cipher cipher;
+ private final byte[] iv;
+
+ T13CC20P1305ReadCipher(final CipherSuite cipherSuite,
+ final SecretKey baseSecret, final SecretKey key,
+ final byte[] iv, final SecretKey hp, final int keyPhase)
+ throws GeneralSecurityException {
+ super(cipherSuite, baseSecret, new T13CC20HPCipher(hp), keyPhase);
+ this.key = key;
+ this.iv = iv;
+ this.cipher = Cipher.getInstance("ChaCha20-Poly1305");
+ }
+
+ @Override
+ protected void doDecrypt(long packetNumber, ByteBuffer packet,
+ int headerLength, ByteBuffer output)
+ throws AEADBadTagException, ShortBufferException, QuicTransportException {
+ byte[] iv = this.iv.clone();
+
+ // apply packet number to IV
+ int i = 11;
+ while (packetNumber > 0) {
+ iv[i] ^= (byte) (packetNumber & 0xFF);
+ packetNumber = packetNumber >>> 8;
+ i--;
+ }
+ final IvParameterSpec ivSpec = new IvParameterSpec(iv);
+ synchronized (cipher) {
+ try {
+ cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);
+ int limit = packet.limit();
+ packet.limit(packet.position() + headerLength);
+ cipher.updateAAD(packet);
+ packet.limit(limit);
+ cipher.doFinal(packet, output);
+ } catch (AEADBadTagException | ShortBufferException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new QuicTransportException("Decryption failed",
+ null, 0, BASE_CRYPTO_ERROR + Alert.INTERNAL_ERROR.id, e);
+ }
+ }
+ }
+
+ @Override
+ long integrityLimit() {
+ return INTEGRITY_LIMIT;
+ }
+
+ @Override
+ protected final void doDiscard() {
+ safeDiscard(this.key);
+ }
+ }
+
+ static final class T13CC20P1305WriteCipher extends QuicWriteCipher {
+ private static final String CIPHER_ALGORITHM_NAME = "ChaCha20-Poly1305";
+ private static final long CONFIDENTIALITY_LIMIT;
+
+ static {
+ // RFC-9001, section 6.6: For AEAD_CHACHA20_POLY1305, the
+ // confidentiality limit is greater than the number of possible
+ // packets (2^62) and so can be disregarded.
+ final long defaultVal = -1; // no limit
+ long limit =
+ KEY_LIMITS.getOrDefault(CIPHER_ALGORITHM_NAME, defaultVal);
+ limit = limit < 0 ? -1 /* no limit */ : limit;
+ CONFIDENTIALITY_LIMIT = limit;
+ }
+
+ private final SecretKey key;
+ private final Cipher cipher;
+ private final byte[] iv;
+
+ T13CC20P1305WriteCipher(final CipherSuite cipherSuite,
+ final SecretKey baseSecret, final SecretKey key,
+ final byte[] iv, final SecretKey hp,
+ final int keyPhase)
+ throws GeneralSecurityException {
+ super(cipherSuite, baseSecret, new T13CC20HPCipher(hp), keyPhase);
+ this.key = key;
+ this.iv = iv;
+ this.cipher = Cipher.getInstance(CIPHER_ALGORITHM_NAME);
+ }
+
+ @Override
+ void doEncryptPacket(final long packetNumber, final ByteBuffer packetHeader,
+ final ByteBuffer packetPayload, final ByteBuffer output)
+ throws ShortBufferException, QuicTransportException {
+ byte[] iv = this.iv.clone();
+
+ // apply packet number to IV
+ int i = 11;
+ long pn = packetNumber;
+ while (pn > 0) {
+ iv[i] ^= (byte) (pn & 0xFF);
+ pn = pn >>> 8;
+ i--;
+ }
+ final IvParameterSpec ivSpec = new IvParameterSpec(iv);
+ synchronized (cipher) {
+ try {
+ cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);
+ cipher.updateAAD(packetHeader);
+ cipher.doFinal(packetPayload, output);
+ } catch (ShortBufferException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new QuicTransportException("Encryption failed",
+ null, 0, BASE_CRYPTO_ERROR + Alert.INTERNAL_ERROR.id, e);
+ }
+ }
+ }
+
+ @Override
+ long confidentialityLimit() {
+ return CONFIDENTIALITY_LIMIT;
+ }
+
+ @Override
+ protected final void doDiscard() {
+ safeDiscard(this.key);
+ }
+ }
+
+ static final class T13CC20HPCipher extends QuicHeaderProtectionCipher {
+ private final Cipher cipher;
+
+ T13CC20HPCipher(final SecretKey hp) throws GeneralSecurityException {
+ super(hp);
+ cipher = Cipher.getInstance("ChaCha20");
+ }
+
+ @Override
+ public ByteBuffer computeHeaderProtectionMask(ByteBuffer sample)
+ throws QuicTransportException {
+ if (sample.remaining() != getHeaderProtectionSampleSize()) {
+ throw new IllegalArgumentException("Invalid sample size");
+ }
+ try {
+ // RFC 7539: [counter is a] 32-bit block count parameter,
+ // treated as a 32-bit little-endian integer
+ // RFC 9001:
+ // counter = sample[0..3]
+ // nonce = sample[4..15]
+ // mask = ChaCha20(hp_key, counter, nonce, {0,0,0,0,0})
+
+ sample.order(ByteOrder.LITTLE_ENDIAN);
+ byte[] nonce = new byte[12];
+ int counter = sample.getInt();
+ sample.get(nonce);
+ ChaCha20ParameterSpec ivSpec =
+ new ChaCha20ParameterSpec(nonce, counter);
+ byte[] output = new byte[5];
+
+ synchronized (cipher) {
+ // DECRYPT produces the same output as ENCRYPT, but does
+ // not throw when the same IV is used repeatedly
+ cipher.init(Cipher.DECRYPT_MODE, headerProtectionKey,
+ ivSpec);
+ int numBytes = cipher.doFinal(output, 0, 5, output);
+ assert numBytes == 5;
+ }
+ return ByteBuffer.wrap(output);
+ } catch (Exception e) {
+ throw new QuicTransportException("Encryption failed",
+ null, 0, BASE_CRYPTO_ERROR + Alert.INTERNAL_ERROR.id, e);
+ }
+ }
+ }
+}
diff --git a/src/java.base/share/classes/sun/security/ssl/QuicEngineOutputRecord.java b/src/java.base/share/classes/sun/security/ssl/QuicEngineOutputRecord.java
new file mode 100644
index 00000000000..893eb282116
--- /dev/null
+++ b/src/java.base/share/classes/sun/security/ssl/QuicEngineOutputRecord.java
@@ -0,0 +1,245 @@
+/*
+ * Copyright (c) 2022, 2025, 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. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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 sun.security.ssl;
+
+import jdk.internal.net.quic.QuicTLSEngine;
+import sun.security.ssl.SSLCipher.SSLWriteCipher;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.LinkedList;
+
+/**
+ * {@code OutputRecord} implementation for {@code QuicTLSEngineImpl}.
+ */
+final class QuicEngineOutputRecord extends OutputRecord implements SSLRecord {
+
+ private final HandshakeFragment fragmenter = new HandshakeFragment();
+
+ private volatile boolean isCloseWaiting;
+
+ private Alert alert;
+
+ QuicEngineOutputRecord(HandshakeHash handshakeHash) {
+ super(handshakeHash, SSLWriteCipher.nullTlsWriteCipher());
+
+ this.packetSize = SSLRecord.maxRecordSize;
+ this.protocolVersion = ProtocolVersion.NONE;
+ }
+
+ @Override
+ public void close() throws IOException {
+ recordLock.lock();
+ try {
+ if (!isClosed) {
+ if (!fragmenter.isEmpty()) {
+ isCloseWaiting = true;
+ } else {
+ super.close();
+ }
+ }
+ } finally {
+ recordLock.unlock();
+ }
+ }
+
+ boolean isClosed() {
+ return isClosed || isCloseWaiting;
+ }
+
+ @Override
+ void encodeAlert(byte level, byte description) throws IOException {
+ recordLock.lock();
+ try {
+ if (isClosed()) {
+ if (SSLLogger.isOn && SSLLogger.isOn("ssl")) {
+ SSLLogger.warning("outbound has closed, ignore outbound " +
+ "alert message: " + Alert.nameOf(description));
+ }
+ return;
+ }
+ if (level == Alert.Level.WARNING.level) {
+ if (SSLLogger.isOn && SSLLogger.isOn("ssl")) {
+ SSLLogger.warning("Suppressing warning-level " +
+ "alert message: " + Alert.nameOf(description));
+ }
+ return;
+ }
+
+ if (alert != null) {
+ if (SSLLogger.isOn && SSLLogger.isOn("ssl")) {
+ SSLLogger.warning("Suppressing subsequent alert: " +
+ description + ", original: " + alert.id);
+ }
+ return;
+ }
+
+ alert = Alert.valueOf(description);
+ } finally {
+ recordLock.unlock();
+ }
+ }
+
+ @Override
+ void encodeHandshake(byte[] source,
+ int offset, int length) throws IOException {
+ recordLock.lock();
+ try {
+ if (isClosed()) {
+ if (SSLLogger.isOn && SSLLogger.isOn("ssl")) {
+ SSLLogger.warning("outbound has closed, ignore outbound " +
+ "handshake message",
+ ByteBuffer.wrap(source, offset, length));
+ }
+ return;
+ }
+
+ firstMessage = false;
+
+ byte handshakeType = source[offset];
+ if (handshakeHash.isHashable(handshakeType)) {
+ handshakeHash.deliver(source, offset, length);
+ }
+
+ fragmenter.queueUpFragment(source, offset, length);
+ } finally {
+ recordLock.unlock();
+ }
+ }
+
+ @Override
+ void encodeChangeCipherSpec() throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ void changeWriteCiphers(SSLWriteCipher writeCipher, boolean useChangeCipherSpec) throws IOException {
+ recordLock.lock();
+ try {
+ fragmenter.changePacketSpace();
+ } finally {
+ recordLock.unlock();
+ }
+ }
+
+ @Override
+ void changeWriteCiphers(SSLWriteCipher writeCipher, byte keyUpdateRequest) throws IOException {
+ throw new UnsupportedOperationException("Should not call this");
+ }
+
+ @Override
+ byte[] getHandshakeMessage() {
+ recordLock.lock();
+ try {
+ return fragmenter.acquireCiphertext();
+ } finally {
+ recordLock.unlock();
+ }
+ }
+
+ @Override
+ QuicTLSEngine.KeySpace getHandshakeMessageKeySpace() {
+ recordLock.lock();
+ try {
+ return switch (fragmenter.currentPacketSpace) {
+ case 0-> QuicTLSEngine.KeySpace.INITIAL;
+ case 1-> QuicTLSEngine.KeySpace.HANDSHAKE;
+ case 2-> QuicTLSEngine.KeySpace.ONE_RTT;
+ default -> throw new IllegalStateException("Unexpected state");
+ };
+ } finally {
+ recordLock.unlock();
+ }
+ }
+
+ @Override
+ boolean isEmpty() {
+ recordLock.lock();
+ try {
+ return fragmenter.isEmpty();
+ } finally {
+ recordLock.unlock();
+ }
+ }
+
+ Alert getAlert() {
+ recordLock.lock();
+ try {
+ return alert;
+ } finally {
+ recordLock.unlock();
+ }
+ }
+
+ // buffered record fragment
+ private static class HandshakeMemo {
+ boolean changeSpace;
+ byte[] fragment;
+ }
+
+ static final class HandshakeFragment {
+ private final LinkedList handshakeMemos =
+ new LinkedList<>();
+
+ private int currentPacketSpace;
+
+ void queueUpFragment(byte[] source,
+ int offset, int length) throws IOException {
+ HandshakeMemo memo = new HandshakeMemo();
+
+ memo.fragment = new byte[length];
+ assert Record.getInt24(ByteBuffer.wrap(source, offset + 1, 3))
+ == length - 4 : "Invalid handshake message length";
+ System.arraycopy(source, offset, memo.fragment, 0, length);
+
+ handshakeMemos.add(memo);
+ }
+
+ void changePacketSpace() {
+ HandshakeMemo lastMemo = handshakeMemos.peekLast();
+ if (lastMemo != null) {
+ lastMemo.changeSpace = true;
+ } else {
+ currentPacketSpace++;
+ }
+ }
+
+ byte[] acquireCiphertext() {
+ HandshakeMemo hsMemo = handshakeMemos.pollFirst();
+ if (hsMemo == null) {
+ return null;
+ }
+ if (hsMemo.changeSpace) {
+ currentPacketSpace++;
+ }
+ return hsMemo.fragment;
+ }
+
+ boolean isEmpty() {
+ return handshakeMemos.isEmpty();
+ }
+ }
+}
diff --git a/src/java.base/share/classes/sun/security/ssl/QuicKeyManager.java b/src/java.base/share/classes/sun/security/ssl/QuicKeyManager.java
new file mode 100644
index 00000000000..fb9077af022
--- /dev/null
+++ b/src/java.base/share/classes/sun/security/ssl/QuicKeyManager.java
@@ -0,0 +1,1216 @@
+/*
+ * Copyright (c) 2023, 2025, 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. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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 sun.security.ssl;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.security.NoSuchAlgorithmException;
+import java.util.HexFormat;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.function.IntFunction;
+
+import javax.crypto.AEADBadTagException;
+import javax.crypto.Cipher;
+import javax.crypto.KDF;
+import javax.crypto.SecretKey;
+import javax.crypto.ShortBufferException;
+import javax.crypto.spec.GCMParameterSpec;
+import javax.crypto.spec.HKDFParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLHandshakeException;
+
+import jdk.internal.net.quic.QuicKeyUnavailableException;
+import jdk.internal.net.quic.QuicOneRttContext;
+import jdk.internal.net.quic.QuicTLSEngine;
+import jdk.internal.net.quic.QuicTransportException;
+import jdk.internal.net.quic.QuicVersion;
+import jdk.internal.vm.annotation.Stable;
+import sun.security.ssl.QuicCipher.QuicReadCipher;
+import sun.security.ssl.QuicCipher.QuicWriteCipher;
+import sun.security.util.KeyUtil;
+
+import static jdk.internal.net.quic.QuicTLSEngine.KeySpace.HANDSHAKE;
+import static jdk.internal.net.quic.QuicTLSEngine.KeySpace.INITIAL;
+import static jdk.internal.net.quic.QuicTLSEngine.KeySpace.ONE_RTT;
+import static jdk.internal.net.quic.QuicTransportErrors.AEAD_LIMIT_REACHED;
+import static jdk.internal.net.quic.QuicTransportErrors.KEY_UPDATE_ERROR;
+import static sun.security.ssl.QuicTLSEngineImpl.BASE_CRYPTO_ERROR;
+
+sealed abstract class QuicKeyManager
+ permits QuicKeyManager.HandshakeKeyManager,
+ QuicKeyManager.InitialKeyManager, QuicKeyManager.OneRttKeyManager {
+
+ private record QuicKeys(SecretKey key, byte[] iv, SecretKey hp) {
+ }
+
+ private record CipherPair(QuicReadCipher readCipher,
+ QuicWriteCipher writeCipher) {
+ void discard(boolean destroyHP) {
+ writeCipher.discard(destroyHP);
+ readCipher.discard(destroyHP);
+ }
+
+ /**
+ * {@return true if the keys represented by this {@code CipherPair}
+ * were used by both this endpoint and the peer, thus implying these
+ * keys are available to both of them}
+ */
+ boolean usedByBothEndpoints() {
+ return this.readCipher.hasDecryptedAny() &&
+ this.writeCipher.hasEncryptedAny();
+ }
+ }
+
+ final QuicTLSEngine.KeySpace keySpace;
+ // counter towards the integrity limit
+ final AtomicLong invalidPackets = new AtomicLong();
+ volatile boolean keysDiscarded;
+
+ private QuicKeyManager(final QuicTLSEngine.KeySpace keySpace) {
+ this.keySpace = keySpace;
+ }
+
+ protected abstract boolean keysAvailable();
+
+ protected abstract QuicReadCipher getReadCipher()
+ throws QuicKeyUnavailableException;
+
+ protected abstract QuicWriteCipher getWriteCipher()
+ throws QuicKeyUnavailableException;
+
+ abstract void discardKeys();
+
+ void decryptPacket(final long packetNumber, final int keyPhase,
+ final ByteBuffer packet,final int headerLength,
+ final ByteBuffer output) throws QuicKeyUnavailableException,
+ IllegalArgumentException, AEADBadTagException,
+ QuicTransportException, ShortBufferException {
+ // keyPhase is only applicable for 1-RTT packets; the decryptPacket
+ // method is overridden by OneRttKeyManager, so this check is for
+ // other packet types
+ if (keyPhase != -1) {
+ throw new IllegalArgumentException(
+ "Unexpected key phase value: " + keyPhase);
+ }
+ // use current keys to decrypt
+ QuicReadCipher readCipher = getReadCipher();
+ try {
+ readCipher.decryptPacket(packetNumber, packet, headerLength, output);
+ } catch (AEADBadTagException e) {
+ if (invalidPackets.incrementAndGet() >=
+ readCipher.integrityLimit()) {
+ throw new QuicTransportException("Integrity limit reached",
+ keySpace, 0, AEAD_LIMIT_REACHED);
+ }
+ throw e;
+ }
+ }
+
+ void encryptPacket(final long packetNumber,
+ final IntFunction headerGenerator,
+ final ByteBuffer packetPayload,
+ final ByteBuffer output)
+ throws QuicKeyUnavailableException, QuicTransportException, ShortBufferException {
+ // generate the packet header passing the generator the key phase
+ final ByteBuffer header = headerGenerator.apply(0); // key phase is always 0 for non-ONE_RTT
+ getWriteCipher().encryptPacket(packetNumber, header, packetPayload, output);
+ }
+
+ private static QuicKeys deriveQuicKeys(final QuicVersion quicVersion,
+ final CipherSuite cs, final SecretKey traffic_secret)
+ throws IOException {
+ final SSLKeyDerivation kd = new QuicTLSKeyDerivation(cs,
+ traffic_secret);
+ final QuicTLSData tlsData = getQuicData(quicVersion);
+ final SecretKey quic_key = kd.deriveKey(tlsData.getTlsKeyLabel());
+ final byte[] quic_iv = kd.deriveData(tlsData.getTlsIvLabel());
+ final SecretKey quic_hp = kd.deriveKey(tlsData.getTlsHpLabel());
+ return new QuicKeys(quic_key, quic_iv, quic_hp);
+ }
+
+ // Used in 1RTT when advancing the keyphase. quic_hp is not advanced.
+ private static QuicKeys deriveQuicKeys(final QuicVersion quicVersion,
+ final CipherSuite cs, final SecretKey traffic_secret,
+ final SecretKey quic_hp) throws IOException {
+ final SSLKeyDerivation kd = new QuicTLSKeyDerivation(cs,
+ traffic_secret);
+ final QuicTLSData tlsData = getQuicData(quicVersion);
+ final SecretKey quic_key = kd.deriveKey(tlsData.getTlsKeyLabel());
+ final byte[] quic_iv = kd.deriveData(tlsData.getTlsIvLabel());
+ return new QuicKeys(quic_key, quic_iv, quic_hp);
+ }
+
+ private static QuicTLSData getQuicData(final QuicVersion quicVersion) {
+ return switch (quicVersion) {
+ case QUIC_V1 -> QuicTLSData.V1;
+ case QUIC_V2 -> QuicTLSData.V2;
+ };
+ }
+
+ private static byte[] createHkdfInfo(final String label, final int length) {
+ final byte[] tls13Label =
+ ("tls13 " + label).getBytes(StandardCharsets.UTF_8);
+ return createHkdfInfo(tls13Label, length);
+ }
+
+ private static byte[] createHkdfInfo(final byte[] tls13Label,
+ final int length) {
+ final byte[] info = new byte[4 + tls13Label.length];
+ final ByteBuffer m = ByteBuffer.wrap(info);
+ try {
+ Record.putInt16(m, length);
+ Record.putBytes8(m, tls13Label);
+ Record.putInt8(m, 0x00); // zero-length context
+ } catch (IOException ioe) {
+ // unlikely
+ throw new UncheckedIOException("Unexpected exception", ioe);
+ }
+ return info;
+ }
+
+ static final class InitialKeyManager extends QuicKeyManager {
+
+ private volatile CipherPair cipherPair;
+
+ InitialKeyManager() {
+ super(INITIAL);
+ }
+
+ @Override
+ protected boolean keysAvailable() {
+ return this.cipherPair != null && !this.keysDiscarded;
+ }
+
+ @Override
+ protected QuicReadCipher getReadCipher()
+ throws QuicKeyUnavailableException {
+ final CipherPair pair = this.cipherPair;
+ if (pair == null) {
+ final String msg = this.keysDiscarded
+ ? "Keys have been discarded"
+ : "Keys not available";
+ throw new QuicKeyUnavailableException(msg, this.keySpace);
+ }
+ return pair.readCipher;
+ }
+
+ @Override
+ protected QuicWriteCipher getWriteCipher()
+ throws QuicKeyUnavailableException {
+ final CipherPair pair = this.cipherPair;
+ if (pair == null) {
+ final String msg = this.keysDiscarded
+ ? "Keys have been discarded"
+ : "Keys not available";
+ throw new QuicKeyUnavailableException(msg, this.keySpace);
+ }
+ return pair.writeCipher;
+ }
+
+ @Override
+ void discardKeys() {
+ final CipherPair toDiscard = this.cipherPair;
+ this.keysDiscarded = true;
+ this.cipherPair = null; // no longer needed
+ if (toDiscard == null) {
+ return;
+ }
+ if (SSLLogger.isOn && SSLLogger.isOn("ssl")) {
+ SSLLogger.finest("discarding keys (keyphase="
+ + toDiscard.writeCipher.getKeyPhase()
+ + ") of " + this.keySpace + " key space");
+ }
+ toDiscard.discard(true);
+ }
+
+ void deriveKeys(final QuicVersion quicVersion,
+ final byte[] connectionId,
+ final boolean clientMode) throws IOException{
+ Objects.requireNonNull(quicVersion);
+ final CipherSuite cs = CipherSuite.TLS_AES_128_GCM_SHA256;
+ final CipherSuite.HashAlg hashAlg = cs.hashAlg;
+
+ KDF hkdf;
+ try {
+ hkdf = KDF.getInstance(hashAlg.hkdfAlgorithm);
+ } catch (NoSuchAlgorithmException e) {
+ throw new SSLHandshakeException("Could not generate secret", e);
+ }
+ final QuicTLSData tlsData = QuicKeyManager.getQuicData(quicVersion);
+ SecretKey initial_secret = null;
+ SecretKey server_initial_secret = null;
+ SecretKey client_initial_secret = null;
+ try {
+ initial_secret = hkdf.deriveKey("TlsInitialSecret",
+ HKDFParameterSpec.ofExtract()
+ .addSalt(tlsData.getInitialSalt())
+ .addIKM(connectionId).extractOnly());
+
+ byte[] clientInfo = createHkdfInfo("client in",
+ hashAlg.hashLength);
+ client_initial_secret =
+ hkdf.deriveKey("TlsClientInitialTrafficSecret",
+ HKDFParameterSpec.expandOnly(
+ initial_secret,
+ clientInfo,
+ hashAlg.hashLength));
+ QuicKeys clientKeys = deriveQuicKeys(quicVersion, cs,
+ client_initial_secret);
+
+ byte[] serverInfo = createHkdfInfo("server in",
+ hashAlg.hashLength);
+ server_initial_secret =
+ hkdf.deriveKey("TlsServerInitialTrafficSecret",
+ HKDFParameterSpec.expandOnly(
+ initial_secret,
+ serverInfo,
+ hashAlg.hashLength));
+ QuicKeys serverKeys = deriveQuicKeys(quicVersion, cs,
+ server_initial_secret);
+
+ final QuicReadCipher readCipher;
+ final QuicWriteCipher writeCipher;
+ final int keyPhase = 0;
+ if (clientMode) {
+ readCipher = QuicCipher.createReadCipher(cs,
+ server_initial_secret,
+ serverKeys.key, serverKeys.iv, serverKeys.hp,
+ keyPhase);
+ writeCipher = QuicCipher.createWriteCipher(cs,
+ client_initial_secret,
+ clientKeys.key, clientKeys.iv, clientKeys.hp,
+ keyPhase);
+ } else {
+ readCipher = QuicCipher.createReadCipher(cs,
+ client_initial_secret,
+ clientKeys.key, clientKeys.iv, clientKeys.hp,
+ keyPhase);
+ writeCipher = QuicCipher.createWriteCipher(cs,
+ server_initial_secret,
+ serverKeys.key, serverKeys.iv, serverKeys.hp,
+ keyPhase);
+ }
+ final CipherPair old = this.cipherPair;
+ // we don't check if keys are already available, since it's a
+ // valid case where the INITIAL keys are regenerated due to a
+ // RETRY packet from the peer or even for the case where a
+ // different quic version was negotiated by the server
+ this.cipherPair = new CipherPair(readCipher, writeCipher);
+ if (old != null) {
+ old.discard(true);
+ }
+ } catch (GeneralSecurityException e) {
+ throw new SSLException("Missing cipher algorithm", e);
+ } finally {
+ KeyUtil.destroySecretKeys(initial_secret, client_initial_secret,
+ server_initial_secret);
+ }
+ }
+
+ static Cipher getRetryCipher(final QuicVersion quicVersion,
+ final boolean incoming) throws QuicTransportException {
+ final QuicTLSData tlsData = QuicKeyManager.getQuicData(quicVersion);
+ return tlsData.getRetryCipher(incoming);
+ }
+ }
+
+ static final class HandshakeKeyManager extends QuicKeyManager {
+ private volatile CipherPair cipherPair;
+
+ HandshakeKeyManager() {
+ super(HANDSHAKE);
+ }
+
+ @Override
+ protected boolean keysAvailable() {
+ return this.cipherPair != null && !this.keysDiscarded;
+ }
+
+ @Override
+ protected QuicReadCipher getReadCipher()
+ throws QuicKeyUnavailableException {
+ final CipherPair pair = this.cipherPair;
+ if (pair == null) {
+ final String msg = this.keysDiscarded
+ ? "Keys have been discarded"
+ : "Keys not available";
+ throw new QuicKeyUnavailableException(msg, this.keySpace);
+ }
+ return pair.readCipher;
+ }
+
+ @Override
+ protected QuicWriteCipher getWriteCipher()
+ throws QuicKeyUnavailableException {
+ final CipherPair pair = this.cipherPair;
+ if (pair == null) {
+ final String msg = this.keysDiscarded
+ ? "Keys have been discarded"
+ : "Keys not available";
+ throw new QuicKeyUnavailableException(msg, this.keySpace);
+ }
+ return pair.writeCipher;
+ }
+
+ @Override
+ void discardKeys() {
+ final CipherPair toDiscard = this.cipherPair;
+ this.cipherPair = null; // no longer needed
+ this.keysDiscarded = true;
+ if (toDiscard == null) {
+ return;
+ }
+ if (SSLLogger.isOn && SSLLogger.isOn("ssl")) {
+ SSLLogger.finest("discarding keys (keyphase="
+ + toDiscard.writeCipher.getKeyPhase()
+ + ") of " + this.keySpace + " key space");
+ }
+ toDiscard.discard(true);
+ }
+
+ void deriveKeys(final QuicVersion quicVersion,
+ final HandshakeContext handshakeContext,
+ final boolean clientMode) throws IOException {
+ Objects.requireNonNull(quicVersion);
+ if (keysAvailable()) {
+ throw new IllegalStateException(
+ "Keys already derived for " + this.keySpace +
+ " key space");
+ }
+ SecretKey client_handshake_traffic_secret = null;
+ SecretKey server_handshake_traffic_secret = null;
+ try {
+ final SSLKeyDerivation kd =
+ handshakeContext.handshakeKeyDerivation;
+ client_handshake_traffic_secret = kd.deriveKey(
+ "TlsClientHandshakeTrafficSecret");
+ final QuicKeys clientKeys = deriveQuicKeys(quicVersion,
+ handshakeContext.negotiatedCipherSuite,
+ client_handshake_traffic_secret);
+ server_handshake_traffic_secret = kd.deriveKey(
+ "TlsServerHandshakeTrafficSecret");
+ final QuicKeys serverKeys = deriveQuicKeys(quicVersion,
+ handshakeContext.negotiatedCipherSuite,
+ server_handshake_traffic_secret);
+
+ final CipherSuite negotiatedCipherSuite =
+ handshakeContext.negotiatedCipherSuite;
+ final QuicReadCipher readCipher;
+ final QuicWriteCipher writeCipher;
+ final int keyPhase = 0;
+ if (clientMode) {
+ readCipher =
+ QuicCipher.createReadCipher(negotiatedCipherSuite,
+ server_handshake_traffic_secret,
+ serverKeys.key, serverKeys.iv,
+ serverKeys.hp, keyPhase);
+ writeCipher =
+ QuicCipher.createWriteCipher(negotiatedCipherSuite,
+ client_handshake_traffic_secret,
+ clientKeys.key, clientKeys.iv,
+ clientKeys.hp, keyPhase);
+ } else {
+ readCipher =
+ QuicCipher.createReadCipher(negotiatedCipherSuite,
+ client_handshake_traffic_secret,
+ clientKeys.key, clientKeys.iv,
+ clientKeys.hp, keyPhase);
+ writeCipher =
+ QuicCipher.createWriteCipher(negotiatedCipherSuite,
+ server_handshake_traffic_secret,
+ serverKeys.key, serverKeys.iv,
+ serverKeys.hp, keyPhase);
+ }
+ synchronized (this) {
+ if (this.cipherPair != null) {
+ // don't allow setting more than once
+ throw new IllegalStateException("Keys already " +
+ "available for keyspace: "
+ + this.keySpace);
+ }
+ this.cipherPair = new CipherPair(readCipher, writeCipher);
+ }
+ } catch (GeneralSecurityException e) {
+ throw new SSLException("Missing cipher algorithm", e);
+ } finally {
+ KeyUtil.destroySecretKeys(client_handshake_traffic_secret,
+ server_handshake_traffic_secret);
+ }
+ }
+ }
+
+ static final class OneRttKeyManager extends QuicKeyManager {
+ // a series of keys that the 1-RTT key manager uses
+ private record KeySeries(QuicReadCipher old, CipherPair current,
+ CipherPair next) {
+ private KeySeries {
+ Objects.requireNonNull(current);
+ if (old != null) {
+ if (old.getKeyPhase() ==
+ current.writeCipher.getKeyPhase()) {
+ throw new IllegalArgumentException("Both old keys and" +
+ " current keys have the same key phase: " +
+ current.writeCipher.getKeyPhase());
+ }
+ }
+ if (next != null) {
+ if (next.writeCipher.getKeyPhase() ==
+ current.writeCipher.getKeyPhase()) {
+ throw new IllegalArgumentException("Both next keys " +
+ "and current keys have the same key phase: " +
+ current.writeCipher.getKeyPhase());
+ }
+ }
+ }
+
+ /**
+ * {@return true if this {@code KeySeries} has an old decryption key
+ * and the {@code pktNum} is lower than the least packet number the
+ * current decryption key has decrypted so far}
+ *
+ * @param pktNum the packet number for which the old key
+ * might be needed
+ */
+ boolean canUseOldDecryptKey(final long pktNum) {
+ assert pktNum >= 0 : "unexpected packet number: " + pktNum;
+ if (this.old == null) {
+ return false;
+ }
+ final QuicReadCipher currentKey = this.current.readCipher;
+ final long lowestDecrypted = currentKey.lowestDecryptedPktNum();
+ // if the incoming packet number is lesser than the lowest
+ // decrypted packet number by the current key, then it
+ // implies that this might be a delayed packet and thus is
+ // allowed to use the old key (if available) from
+ // the previous key phase.
+ // see RFC-9001, section 6.5
+ if (lowestDecrypted == -1) {
+ return true;
+ }
+ return pktNum < lowestDecrypted;
+ }
+ }
+
+ // will be set when the keys are derived
+ private volatile QuicVersion negotiatedVersion;
+
+ private final Lock keySeriesLock = new ReentrantLock();
+ // will be set when keys are derived and will
+ // be updated whenever keys are updated.
+ // Must be updated/written only
+ // when holding the keySeriesLock lock
+ private volatile KeySeries keySeries;
+
+ @Stable
+ private volatile QuicOneRttContext oneRttContext;
+
+ OneRttKeyManager() {
+ super(ONE_RTT);
+ }
+
+ @Override
+ protected boolean keysAvailable() {
+ return this.keySeries != null && !this.keysDiscarded;
+ }
+
+ @Override
+ protected QuicReadCipher getReadCipher()
+ throws QuicKeyUnavailableException {
+ final KeySeries series = requireKeySeries();
+ return series.current.readCipher;
+ }
+
+ @Override
+ protected QuicWriteCipher getWriteCipher()
+ throws QuicKeyUnavailableException {
+ final KeySeries series = requireKeySeries();
+ return series.current.writeCipher;
+ }
+
+ @Override
+ void discardKeys() {
+ this.keysDiscarded = true;
+ final KeySeries series;
+ this.keySeriesLock.lock();
+ try {
+ series = this.keySeries;
+ this.keySeries = null; // no longer available
+ } finally {
+ this.keySeriesLock.unlock();
+ }
+ if (series == null) {
+ return;
+ }
+ if (SSLLogger.isOn && SSLLogger.isOn("ssl")) {
+ SSLLogger.finest("discarding key (series) of " +
+ this.keySpace + " key space");
+ }
+ if (series.old != null) {
+ series.old.discard(false);
+ }
+ discardKeys(series.current);
+ discardKeys(series.next);
+ }
+
+ @Override
+ void decryptPacket(final long packetNumber, final int keyPhase,
+ final ByteBuffer packet, final int headerLength,
+ final ByteBuffer output) throws QuicKeyUnavailableException,
+ QuicTransportException, AEADBadTagException, ShortBufferException {
+ if (keyPhase != 0 && keyPhase != 1) {
+ throw new IllegalArgumentException("Unexpected key phase " +
+ "value: " + keyPhase);
+ }
+ final KeySeries series = requireKeySeries();
+ final CipherPair current = series.current;
+ // Use the write cipher's key phase to detect a key update as noted
+ // in RFC-9001, section 6.2:
+ // An endpoint detects a key update when processing a packet with
+ // a key phase that differs from the value used to protect the
+ // last packet it sent.
+ final int currentKeyPhase = current.writeCipher.getKeyPhase();
+ if (keyPhase == currentKeyPhase) {
+ current.readCipher.decryptPacket(packetNumber, packet,
+ headerLength, output);
+ return;
+ }
+ // incoming packet is using a key phase which doesn't match the
+ // current key phase. this implies that either a key update
+ // is being initiated or a key update initiated by the current
+ // endpoint is in progress and some older packet with the
+ // previous key phase has arrived.
+ if (series.canUseOldDecryptKey(packetNumber)) {
+ final QuicReadCipher oldReadCipher = series.old;
+ assert oldReadCipher != null : "old key is unexpectedly null";
+ if (SSLLogger.isOn && SSLLogger.isOn("ssl")) {
+ SSLLogger.finest("using old read key to decrypt packet: " +
+ packetNumber + ", with incoming key phase: " +
+ keyPhase + ", current key phase: " +
+ currentKeyPhase);
+ }
+ oldReadCipher.decryptPacket(
+ packetNumber, packet, headerLength, output);
+ // we were able to decrypt using an old key. now verify
+ // that it was OK to use this old key for this packet.
+ if (!series.current.usedByBothEndpoints()
+ && series.current.writeCipher.hasEncryptedAny()
+ && oneRttContext.getLargestPeerAckedPN()
+ >= series.current.writeCipher.lowestEncryptedPktNum()) {
+ // RFC-9001, section 6.2:
+ // An endpoint that receives an acknowledgment that is
+ // carried in a packet protected with old keys where any
+ // acknowledged packet was protected with newer keys MAY
+ // treat that as a connection error of type
+ // KEY_UPDATE_ERROR. This indicates that a peer has
+ // received and acknowledged a packet that initiates a key
+ // update, but has not updated keys in response.
+ if (SSLLogger.isOn && SSLLogger.isOn("ssl")) {
+ SSLLogger.finest("peer used incorrect key, was" +
+ " expected to use updated key of" +
+ " key phase: " + currentKeyPhase +
+ ", incoming key phase: " + keyPhase +
+ ", packet number: " + packetNumber);
+ }
+ throw new QuicTransportException("peer used incorrect" +
+ " key, was expected to use updated key",
+ this.keySpace, 0, KEY_UPDATE_ERROR);
+ }
+ return;
+ }
+ if (SSLLogger.isOn && SSLLogger.isOn("ssl")) {
+ SSLLogger.finest("detected ONE_RTT key update, current key " +
+ "phase: " + currentKeyPhase
+ + ", incoming key phase: " + keyPhase
+ + ", packet number: " + packetNumber);
+ }
+ decryptUsingNextKeys(
+ series, packetNumber, packet, headerLength, output);
+ }
+
+ @Override
+ void encryptPacket(final long packetNumber,
+ final IntFunction headerGenerator,
+ final ByteBuffer packetPayload,
+ final ByteBuffer output)
+ throws QuicKeyUnavailableException, QuicTransportException, ShortBufferException {
+ KeySeries currentSeries = requireKeySeries();
+ if (currentSeries.next == null) {
+ // next keys haven't yet been generated,
+ // generate them now
+ try {
+ currentSeries = generateNextKeys(
+ this.negotiatedVersion, currentSeries);
+ } catch (GeneralSecurityException | IOException e) {
+ throw new QuicTransportException("Failed to update keys",
+ ONE_RTT, 0, BASE_CRYPTO_ERROR + Alert.INTERNAL_ERROR.id, e);
+ }
+ }
+ maybeInitiateKeyUpdate(currentSeries, packetNumber);
+ // call getWriteCipher() afresh so that it can use
+ // the new keyseries if at all the key update was
+ // initiated
+ final QuicWriteCipher writeCipher = getWriteCipher();
+ final int keyPhase = writeCipher.getKeyPhase();
+ // generate the packet header passing the generator the key phase
+ final ByteBuffer header = headerGenerator.apply(keyPhase);
+ writeCipher.encryptPacket(packetNumber, header, packetPayload, output);
+ }
+
+ void setOneRttContext(final QuicOneRttContext ctx) {
+ Objects.requireNonNull(ctx);
+ this.oneRttContext = ctx;
+ }
+
+ private KeySeries requireKeySeries()
+ throws QuicKeyUnavailableException {
+ final KeySeries series = this.keySeries;
+ if (series != null) {
+ return series;
+ }
+ final String msg = this.keysDiscarded
+ ? "Keys have been discarded"
+ : "Keys not available";
+ throw new QuicKeyUnavailableException(msg, this.keySpace);
+ }
+
+ // based on certain internal criteria, this method may trigger a key
+ // update.
+ // returns true if it does trigger the key update. false otherwise.
+ private boolean maybeInitiateKeyUpdate(final KeySeries currentSeries,
+ final long packetNumber) {
+ final QuicWriteCipher cipher = currentSeries.current.writeCipher;
+ // when we notice that we have reached 80% (which is arbitrary)
+ // of the confidentiality limit, we trigger a key update instead
+ // of waiting to hit the limit
+ final long confidentialityLimit = cipher.confidentialityLimit();
+ if (confidentialityLimit < 0) {
+ return false;
+ }
+ final long numEncrypted = cipher.getNumEncrypted();
+ if (numEncrypted >= 0.8 * confidentialityLimit) {
+ if (SSLLogger.isOn && SSLLogger.isOn("ssl")) {
+ SSLLogger.finest("about to reach confidentiality limit, " +
+ "attempting to initiate a 1-RTT key update," +
+ " packet number: " +
+ packetNumber + ", current key phase: " +
+ cipher.getKeyPhase());
+ }
+ final boolean initiated = initiateKeyUpdate(currentSeries);
+ if (initiated) {
+ final int newKeyPhase =
+ this.keySeries.current.writeCipher.getKeyPhase();
+ assert cipher.getKeyPhase() != newKeyPhase
+ : "key phase of updated key unexpectedly matches " +
+ "the key phase "
+ + cipher.getKeyPhase() + " of current keys";
+ if (SSLLogger.isOn && SSLLogger.isOn("ssl")) {
+ SSLLogger.finest(
+ "1-RTT key update initiated, new key phase: "
+ + newKeyPhase);
+ }
+ }
+ return initiated;
+ }
+ return false;
+ }
+
+ private boolean initiateKeyUpdate(final KeySeries series) {
+ // we only initiate a key update if this current endpoint and the
+ // peer have both been using this current key
+ if (!series.current.usedByBothEndpoints()) {
+ // RFC-9001, section 6.1:
+ // An endpoint MUST NOT initiate a subsequent key update
+ // unless it has received
+ // an acknowledgment for a packet that was sent protected
+ // with keys from the
+ // current key phase. This ensures that keys are
+ // available to both peers before
+ // another key update can be initiated.
+ if (SSLLogger.isOn && SSLLogger.isOn("ssl")) {
+ SSLLogger.finest(
+ "skipping key update initiation because peer " +
+ "hasn't yet sent us a packet encrypted with " +
+ "current key of key phase: " +
+ series.current.readCipher.getKeyPhase());
+ }
+ return false;
+ }
+ // OK to initiate a key update.
+ // An endpoint initiates a key update by updating its packet
+ // protection write secret
+ // and using that to protect new packets.
+ rolloverKeys(this.negotiatedVersion, series);
+ return true;
+ }
+
+ private static void discardKeys(final CipherPair cipherPair) {
+ if (cipherPair == null) {
+ return;
+ }
+ cipherPair.discard(true);
+ }
+
+ /**
+ * uses "next" keys to try and decrypt the incoming packet. if that
+ * succeeded then it implies that the key update was indeed initiated by
+ * the peer and this method then rolls over the keys to start using
+ * these "next" keys. this method then returns true in such cases. if
+ * the packet decryption using the "next" key fails, then this method
+ * just returns back false (and doesn't roll over the keys)
+ */
+ private void decryptUsingNextKeys(
+ final KeySeries currentKeySeries,
+ final long packetNumber,
+ final ByteBuffer packet,
+ final int headerLength,
+ final ByteBuffer output)
+ throws QuicKeyUnavailableException, AEADBadTagException,
+ ShortBufferException, QuicTransportException {
+ if (currentKeySeries.next == null) {
+ // this can happen if the peer initiated another
+ // key update before we could generate the next
+ // keys during our encryption flow. in such
+ // cases we reject the key update for the packet
+ // (we avoid timing attacks by not generating
+ // keys during decryption, our key generation
+ // only happens during encryption)
+ if (SSLLogger.isOn && SSLLogger.isOn("ssl")) {
+ SSLLogger.finest("next keys unavailable," +
+ " won't decrypt a packet which appears to be" +
+ " a key update");
+ }
+ throw new QuicKeyUnavailableException(
+ "next keys unavailable to handle key update",
+ this.keySpace);
+ }
+ // use the next keys to attempt decrypting
+ currentKeySeries.next.readCipher.decryptPacket(packetNumber, packet,
+ headerLength, output);
+ if (SSLLogger.isOn && SSLLogger.isOn("ssl")) {
+ SSLLogger.finest(
+ "decrypted using next keys for peer-initiated" +
+ " key update; will now switch to new key phase: " +
+ currentKeySeries.next.readCipher.getKeyPhase());
+ }
+ // we have successfully decrypted the packet using the new/next
+ // read key. So we now update even the write key as noted in
+ // RFC-9001, section 6.2:
+ // If a packet is successfully processed using the next key and
+ // IV, then the peer has initiated a key update. The endpoint
+ // MUST update its send keys to the corresponding
+ // key phase in response, as described in Section 6.1. Sending
+ // keys MUST be updated before sending an acknowledgment for the
+ // packet that was received with updated keys. rollover the
+ // keys == old gets discarded and is replaced by
+ // current, current is replaced by next and next is set to null
+ // (a new set of next keys will be generated separately on
+ // a schedule)
+ rolloverKeys(this.negotiatedVersion, currentKeySeries);
+ }
+
+ void deriveKeys(final QuicVersion negotiatedVersion,
+ final HandshakeContext handshakeContext,
+ final boolean clientMode) throws IOException {
+ Objects.requireNonNull(negotiatedVersion);
+ if (keysAvailable()) {
+ throw new IllegalStateException("Keys already derived for " +
+ this.keySpace + " key space");
+ }
+ this.negotiatedVersion = negotiatedVersion;
+
+ try {
+ SSLKeyDerivation kd = handshakeContext.handshakeKeyDerivation;
+ SecretKey client_application_traffic_secret_0 = kd.deriveKey(
+ "TlsClientAppTrafficSecret");
+ SecretKey server_application_traffic_secret_0 = kd.deriveKey(
+ "TlsServerAppTrafficSecret");
+
+ deriveOneRttKeys(this.negotiatedVersion,
+ client_application_traffic_secret_0,
+ server_application_traffic_secret_0,
+ handshakeContext.negotiatedCipherSuite,
+ clientMode);
+ } catch (GeneralSecurityException e) {
+ throw new SSLException("Missing cipher algorithm", e);
+ }
+ }
+
+ void deriveOneRttKeys(final QuicVersion version,
+ final SecretKey client_application_traffic_secret_0,
+ final SecretKey server_application_traffic_secret_0,
+ final CipherSuite negotiatedCipherSuite,
+ final boolean clientMode) throws IOException,
+ GeneralSecurityException {
+ final QuicKeys clientKeys = deriveQuicKeys(version,
+ negotiatedCipherSuite,
+ client_application_traffic_secret_0);
+ final QuicKeys serverKeys = deriveQuicKeys(version,
+ negotiatedCipherSuite,
+ server_application_traffic_secret_0);
+ final QuicReadCipher readCipher;
+ final QuicWriteCipher writeCipher;
+ // this method always derives the first key for the 1-RTT, so key
+ // phase is always 0
+ final int keyPhase = 0;
+ if (clientMode) {
+ readCipher = QuicCipher.createReadCipher(negotiatedCipherSuite,
+ server_application_traffic_secret_0, serverKeys.key,
+ serverKeys.iv, serverKeys.hp, keyPhase);
+ writeCipher =
+ QuicCipher.createWriteCipher(negotiatedCipherSuite,
+ client_application_traffic_secret_0, clientKeys.key,
+ clientKeys.iv, clientKeys.hp, keyPhase);
+ } else {
+ readCipher = QuicCipher.createReadCipher(negotiatedCipherSuite,
+ client_application_traffic_secret_0, clientKeys.key,
+ clientKeys.iv, clientKeys.hp, keyPhase);
+ writeCipher =
+ QuicCipher.createWriteCipher(negotiatedCipherSuite,
+ server_application_traffic_secret_0, serverKeys.key,
+ serverKeys.iv, serverKeys.hp, keyPhase);
+ }
+ // generate the next set of keys beforehand to prevent any timing
+ // attacks
+ // during key update
+ final QuicReadCipher nPlus1ReadCipher =
+ generateNextReadCipher(version, readCipher);
+ final QuicWriteCipher nPlus1WriteCipher =
+ generateNextWriteCipher(version, writeCipher);
+ this.keySeriesLock.lock();
+ try {
+ if (this.keySeries != null) {
+ // don't allow deriving the first set of 1-RTT keys more
+ // than once
+ throw new IllegalStateException("Keys already available " +
+ "for keyspace: "
+ + this.keySpace);
+ }
+ this.keySeries = new KeySeries(null,
+ new CipherPair(readCipher, writeCipher),
+ new CipherPair(nPlus1ReadCipher, nPlus1WriteCipher));
+ } finally {
+ this.keySeriesLock.unlock();
+ }
+ }
+
+ private static QuicWriteCipher generateNextWriteCipher(
+ final QuicVersion quicVersion, final QuicWriteCipher current)
+ throws IOException, GeneralSecurityException {
+ final SSLKeyDerivation kd =
+ new QuicTLSKeyDerivation(current.getCipherSuite(),
+ current.getBaseSecret());
+ final QuicTLSData tlsData = QuicKeyManager.getQuicData(quicVersion);
+ final SecretKey nplus1Secret =
+ kd.deriveKey(tlsData.getTlsKeyUpdateLabel());
+ final QuicKeys quicKeys =
+ QuicKeyManager.deriveQuicKeys(quicVersion,
+ current.getCipherSuite(),
+ nplus1Secret, current.getHeaderProtectionKey());
+ final int nextKeyPhase = current.getKeyPhase() == 0 ? 1 : 0;
+ // toggle the 1 bit keyphase
+ final QuicWriteCipher next =
+ QuicCipher.createWriteCipher(current.getCipherSuite(),
+ nplus1Secret, quicKeys.key, quicKeys.iv,
+ quicKeys.hp, nextKeyPhase);
+ return next;
+ }
+
+ private static QuicReadCipher generateNextReadCipher(
+ final QuicVersion quicVersion, final QuicReadCipher current)
+ throws IOException, GeneralSecurityException {
+ final SSLKeyDerivation kd =
+ new QuicTLSKeyDerivation(current.getCipherSuite(),
+ current.getBaseSecret());
+ final QuicTLSData tlsData = QuicKeyManager.getQuicData(quicVersion);
+ final SecretKey nPlus1Secret =
+ kd.deriveKey(tlsData.getTlsKeyUpdateLabel());
+ final QuicKeys quicKeys =
+ QuicKeyManager.deriveQuicKeys(quicVersion,
+ current.getCipherSuite(),
+ nPlus1Secret, current.getHeaderProtectionKey());
+ final int nextKeyPhase = current.getKeyPhase() == 0 ? 1 : 0;
+ // toggle the 1 bit keyphase
+ final QuicReadCipher next =
+ QuicCipher.createReadCipher(current.getCipherSuite(),
+ nPlus1Secret, quicKeys.key,
+ quicKeys.iv, quicKeys.hp, nextKeyPhase);
+ return next;
+ }
+
+ private KeySeries generateNextKeys(final QuicVersion version,
+ final KeySeries currentSeries)
+ throws GeneralSecurityException, IOException {
+ this.keySeriesLock.lock();
+ try {
+ // nothing to do if some other thread
+ // already changed the keySeries
+ if (this.keySeries != currentSeries) {
+ return this.keySeries;
+ }
+ final QuicReadCipher nPlus1ReadCipher =
+ generateNextReadCipher(version,
+ currentSeries.current.readCipher);
+ final QuicWriteCipher nPlus1WriteCipher =
+ generateNextWriteCipher(version,
+ currentSeries.current.writeCipher);
+ // only the next keys will differ in the new series
+ // as compared to the current series
+ final KeySeries newSeries = new KeySeries(currentSeries.old,
+ currentSeries.current,
+ new CipherPair(nPlus1ReadCipher, nPlus1WriteCipher));
+ this.keySeries = newSeries;
+ return newSeries;
+ } finally {
+ this.keySeriesLock.unlock();
+ }
+ }
+
+ /**
+ * Updates the key series by "left shifting" the series of keys.
+ * i.e. old keys (if any) are discarded, current keys
+ * are moved to old keys and next keys are moved to current keys.
+ * Note that no new keys will be generated by this method.
+ * @return the key series that will be in use going forward
+ */
+ private KeySeries rolloverKeys(final QuicVersion version,
+ final KeySeries currentSeries) {
+ this.keySeriesLock.lock();
+ try {
+ // nothing to do if some other thread
+ // already changed the keySeries
+ if (this.keySeries != currentSeries) {
+ return this.keySeries;
+ }
+ assert currentSeries.next != null : "Key series missing next" +
+ " keys";
+ // discard the old read cipher which will no longer be used
+ final QuicReadCipher oldReadCipher = currentSeries.old;
+ // once we move current key to old, we won't be using the
+ // write cipher of that
+ // moved pair
+ final QuicWriteCipher writeCipherToDiscard =
+ currentSeries.current.writeCipher;
+ final KeySeries newSeries = new KeySeries(
+ currentSeries.current.readCipher, currentSeries.next,
+ null);
+ // update the key series
+ this.keySeries = newSeries;
+ if (oldReadCipher != null) {
+ if (SSLLogger.isOn && SSLLogger.isOn("ssl")) {
+ SSLLogger.finest(
+ "discarding old read key of key phase: " +
+ oldReadCipher.getKeyPhase());
+ }
+ oldReadCipher.discard(false);
+ }
+ if (SSLLogger.isOn && SSLLogger.isOn("ssl")) {
+ SSLLogger.finest("discarding write key of key phase: " +
+ writeCipherToDiscard.getKeyPhase());
+ }
+ writeCipherToDiscard.discard(false);
+ return newSeries;
+ } finally {
+ this.keySeriesLock.unlock();
+ }
+ }
+ }
+
+ private static final class QuicTLSKeyDerivation
+ implements SSLKeyDerivation {
+
+ private enum HkdfLabel {
+ // RFC 9001: quic version 1
+ quickey("quic key"),
+ quiciv("quic iv"),
+ quichp("quic hp"),
+ quicku("quic ku"),
+
+ // RFC 9369: quic version 2
+ quicv2key("quicv2 key"),
+ quicv2iv("quicv2 iv"),
+ quicv2hp("quicv2 hp"),
+ quicv2ku("quicv2 ku");
+
+ private final String label;
+ private final byte[] tls13LabelBytes;
+
+ HkdfLabel(final String label) {
+ Objects.requireNonNull(label);
+ this.label = label;
+ this.tls13LabelBytes =
+ ("tls13 " + label).getBytes(StandardCharsets.UTF_8);
+ }
+
+ private static HkdfLabel fromLabel(final String label) {
+ Objects.requireNonNull(label);
+ for (final HkdfLabel hkdfLabel : HkdfLabel.values()) {
+ if (hkdfLabel.label.equals(label)) {
+ return hkdfLabel;
+ }
+ }
+ throw new IllegalArgumentException(
+ "unrecognized label: " + label);
+ }
+ }
+
+ private final CipherSuite cs;
+ private final SecretKey secret;
+
+ private QuicTLSKeyDerivation(final CipherSuite cs,
+ final SecretKey secret) {
+ this.cs = Objects.requireNonNull(cs);
+ this.secret = Objects.requireNonNull(secret);
+ }
+
+ @Override
+ public SecretKey deriveKey(final String algorithm) throws IOException {
+ final HkdfLabel hkdfLabel = HkdfLabel.fromLabel(algorithm);
+ try {
+ final KDF hkdf = KDF.getInstance(this.cs.hashAlg.hkdfAlgorithm);
+ final int keyLength = getKeyLength(hkdfLabel);
+ final byte[] hkdfInfo =
+ createHkdfInfo(hkdfLabel.tls13LabelBytes, keyLength);
+ final String keyAlgo = getKeyAlgorithm(hkdfLabel);
+ return hkdf.deriveKey(keyAlgo,
+ HKDFParameterSpec.expandOnly(
+ secret, hkdfInfo, keyLength));
+ } catch (GeneralSecurityException gse) {
+ throw new SSLHandshakeException("Could not derive key", gse);
+ }
+ }
+
+ @Override
+ public byte[] deriveData(final String algorithm) throws IOException {
+ final HkdfLabel hkdfLabel = HkdfLabel.fromLabel(algorithm);
+ try {
+ final KDF hkdf = KDF.getInstance(this.cs.hashAlg.hkdfAlgorithm);
+ final int keyLength = getKeyLength(hkdfLabel);
+ final byte[] hkdfInfo =
+ createHkdfInfo(hkdfLabel.tls13LabelBytes, keyLength);
+ return hkdf.deriveData(HKDFParameterSpec.expandOnly(
+ secret, hkdfInfo, keyLength));
+ } catch (GeneralSecurityException gse) {
+ throw new SSLHandshakeException("Could not derive key", gse);
+ }
+ }
+
+ private int getKeyLength(final HkdfLabel hkdfLabel) {
+ return switch (hkdfLabel) {
+ case quicku, quicv2ku -> {
+ // RFC-9001, section 6.1:
+ // secret_ = HKDF-Expand-Label(secret_, "quic
+ // ku", "", Hash.length)
+ yield this.cs.hashAlg.hashLength;
+ }
+ case quiciv, quicv2iv -> this.cs.bulkCipher.ivSize;
+ default -> this.cs.bulkCipher.keySize;
+ };
+ }
+
+ private String getKeyAlgorithm(final HkdfLabel hkdfLabel) {
+ return switch (hkdfLabel) {
+ case quicku, quicv2ku -> "TlsUpdateNplus1";
+ case quiciv, quicv2iv ->
+ throw new IllegalArgumentException("IV not expected");
+ default -> this.cs.bulkCipher.algorithm;
+ };
+ }
+ }
+
+ private enum QuicTLSData {
+ V1("38762cf7f55934b34d179ae6a4c80cadccbb7f0a",
+ "be0c690b9f66575a1d766b54e368c84e",
+ "461599d35d632bf2239825bb",
+ "quic key", "quic iv", "quic hp", "quic ku"),
+ V2("0dede3def700a6db819381be6e269dcbf9bd2ed9",
+ "8fb4b01b56ac48e260fbcbcead7ccc92",
+ "d86969bc2d7c6d9990efb04a",
+ "quicv2 key", "quicv2 iv", "quicv2 hp", "quicv2 ku");
+
+ private final byte[] initialSalt;
+ private final SecretKey retryKey;
+ private final GCMParameterSpec retryIvSpec;
+ private final String keyLabel;
+ private final String ivLabel;
+ private final String hpLabel;
+ private final String kuLabel;
+
+ QuicTLSData(String initialSalt, String retryKey, String retryIv,
+ String keyLabel, String ivLabel, String hpLabel,
+ String kuLabel) {
+ this.initialSalt = HexFormat.of()
+ .parseHex(initialSalt);
+ this.retryKey = new SecretKeySpec(HexFormat.of()
+ .parseHex(retryKey), "AES");
+ retryIvSpec = new GCMParameterSpec(128,
+ HexFormat.of().parseHex(retryIv));
+ this.keyLabel = keyLabel;
+ this.ivLabel = ivLabel;
+ this.hpLabel = hpLabel;
+ this.kuLabel = kuLabel;
+ }
+
+ public byte[] getInitialSalt() {
+ return initialSalt;
+ }
+
+ public Cipher getRetryCipher(boolean incoming) throws QuicTransportException {
+ Cipher retryCipher = null;
+ try {
+ retryCipher = Cipher.getInstance("AES/GCM/NoPadding");
+ retryCipher.init(incoming ? Cipher.DECRYPT_MODE :
+ Cipher.ENCRYPT_MODE,
+ retryKey, retryIvSpec);
+ } catch (Exception e) {
+ throw new QuicTransportException("Cipher not available",
+ null, 0, BASE_CRYPTO_ERROR + Alert.INTERNAL_ERROR.id, e);
+ }
+ return retryCipher;
+ }
+
+ public String getTlsKeyLabel() {
+ return keyLabel;
+ }
+
+ public String getTlsIvLabel() {
+ return ivLabel;
+ }
+
+ public String getTlsHpLabel() {
+ return hpLabel;
+ }
+
+ public String getTlsKeyUpdateLabel() {
+ return kuLabel;
+ }
+ }
+}
diff --git a/src/java.base/share/classes/sun/security/ssl/QuicTLSEngineImpl.java b/src/java.base/share/classes/sun/security/ssl/QuicTLSEngineImpl.java
new file mode 100644
index 00000000000..6765f554fcc
--- /dev/null
+++ b/src/java.base/share/classes/sun/security/ssl/QuicTLSEngineImpl.java
@@ -0,0 +1,893 @@
+/*
+ * Copyright (c) 2021, 2025, 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. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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 sun.security.ssl;
+
+import jdk.internal.net.quic.QuicKeyUnavailableException;
+import jdk.internal.net.quic.QuicOneRttContext;
+import jdk.internal.net.quic.QuicTLSEngine;
+import jdk.internal.net.quic.QuicTransportErrors;
+import jdk.internal.net.quic.QuicTransportException;
+import jdk.internal.net.quic.QuicTransportParametersConsumer;
+import jdk.internal.net.quic.QuicVersion;
+import sun.security.ssl.QuicKeyManager.HandshakeKeyManager;
+import sun.security.ssl.QuicKeyManager.InitialKeyManager;
+import sun.security.ssl.QuicKeyManager.OneRttKeyManager;
+
+import javax.crypto.AEADBadTagException;
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+import javax.crypto.ShortBufferException;
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.SSLParameters;
+import javax.net.ssl.SSLSession;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.VarHandle;
+import java.nio.ByteBuffer;
+import java.security.AlgorithmConstraints;
+import java.security.GeneralSecurityException;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.IntFunction;
+
+import static jdk.internal.net.quic.QuicTLSEngine.HandshakeState.*;
+import static jdk.internal.net.quic.QuicTLSEngine.KeySpace.*;
+
+/**
+ * One instance per QUIC connection. Configuration methods similar to
+ * SSLEngine.
+ *
+ * The implementation of this class uses the {@link QuicKeyManager} to maintain
+ * all state relating to keys for each encryption levels.
+ */
+public final class QuicTLSEngineImpl implements QuicTLSEngine, SSLTransport {
+
+ private static final Map messageTypeMap =
+ Map.of(SSLHandshake.CLIENT_HELLO.id, INITIAL,
+ SSLHandshake.SERVER_HELLO.id, INITIAL,
+ SSLHandshake.ENCRYPTED_EXTENSIONS.id, HANDSHAKE,
+ SSLHandshake.CERTIFICATE_REQUEST.id, HANDSHAKE,
+ SSLHandshake.CERTIFICATE.id, HANDSHAKE,
+ SSLHandshake.CERTIFICATE_VERIFY.id, HANDSHAKE,
+ SSLHandshake.FINISHED.id, HANDSHAKE,
+ SSLHandshake.NEW_SESSION_TICKET.id, ONE_RTT);
+ static final long BASE_CRYPTO_ERROR = 256;
+
+ private static final Set SUPPORTED_QUIC_VERSIONS =
+ Set.of(QuicVersion.QUIC_V1, QuicVersion.QUIC_V2);
+
+ // VarHandles are used to access compareAndSet semantics.
+ private static final VarHandle HANDSHAKE_STATE_HANDLE;
+ static {
+ final MethodHandles.Lookup lookup = MethodHandles.lookup();
+ try {
+ final Class> quicTlsEngineImpl = QuicTLSEngineImpl.class;
+ HANDSHAKE_STATE_HANDLE = lookup.findVarHandle(
+ quicTlsEngineImpl,
+ "handshakeState",
+ HandshakeState.class);
+ } catch (Exception e) {
+ throw new ExceptionInInitializerError(e);
+ }
+ }
+
+ private final TransportContext conContext;
+ private final String peerHost;
+ private final int peerPort;
+ private volatile HandshakeState handshakeState;
+ private volatile KeySpace sendKeySpace;
+ // next message to send or receive
+ private volatile ByteBuffer localQuicTransportParameters;
+ private volatile QuicTransportParametersConsumer
+ remoteQuicTransportParametersConsumer;
+
+ // keymanagers for individual keyspaces
+ private final InitialKeyManager initialKeyManager = new InitialKeyManager();
+ private final HandshakeKeyManager handshakeKeyManager =
+ new HandshakeKeyManager();
+ private final OneRttKeyManager oneRttKeyManager = new OneRttKeyManager();
+
+ // buffer for crypto data that was received but not yet processed (i.e.
+ // incomplete messages)
+ private volatile ByteBuffer incomingCryptoBuffer;
+ // key space for incomingCryptoBuffer
+ private volatile KeySpace incomingCryptoSpace;
+
+ private volatile QuicVersion negotiatedVersion;
+
+ public QuicTLSEngineImpl(SSLContextImpl sslContextImpl) {
+ this(sslContextImpl, null, -1);
+ }
+
+ public QuicTLSEngineImpl(SSLContextImpl sslContextImpl, final String peerHost, final int peerPort) {
+ this.peerHost = peerHost;
+ this.peerPort = peerPort;
+ this.sendKeySpace = INITIAL;
+ HandshakeHash handshakeHash = new HandshakeHash();
+ this.conContext = new TransportContext(sslContextImpl, this,
+ new SSLEngineInputRecord(handshakeHash),
+ new QuicEngineOutputRecord(handshakeHash));
+ conContext.sslConfig.enabledProtocols = List.of(ProtocolVersion.TLS13);
+ if (peerHost != null) {
+ conContext.sslConfig.serverNames =
+ Utilities.addToSNIServerNameList(
+ conContext.sslConfig.serverNames, peerHost);
+ }
+ conContext.setQuic(true);
+ }
+
+ @Override
+ public void setUseClientMode(boolean mode) {
+ conContext.setUseClientMode(mode);
+ this.handshakeState = mode
+ ? HandshakeState.NEED_SEND_CRYPTO
+ : HandshakeState.NEED_RECV_CRYPTO;
+ }
+
+ @Override
+ public boolean getUseClientMode() {
+ return conContext.sslConfig.isClientMode;
+ }
+
+ @Override
+ public void setSSLParameters(final SSLParameters params) {
+ Objects.requireNonNull(params);
+ // section, 4.2 of RFC-9001
+ // Clients MUST NOT offer TLS versions older than 1.3
+ final String[] protos = params.getProtocols();
+ if (protos == null || protos.length == 0) {
+ throw new IllegalArgumentException("No TLS protocols set");
+ }
+ boolean tlsv13Present = false;
+ Set unsupported = new HashSet<>();
+ for (String p : protos) {
+ if ("TLSv1.3".equals(p)) {
+ tlsv13Present = true;
+ } else {
+ unsupported.add(p);
+ }
+ }
+ if (!tlsv13Present) {
+ throw new IllegalArgumentException(
+ "required TLSv1.3 protocol version hasn't been set");
+ }
+ if (!unsupported.isEmpty()) {
+ throw new IllegalArgumentException(
+ "Unsupported TLS protocol versions " + unsupported);
+ }
+ conContext.sslConfig.setSSLParameters(params);
+ }
+
+ @Override
+ public SSLSession getSession() {
+ return conContext.conSession;
+ }
+
+ @Override
+ public SSLSession getHandshakeSession() {
+ final HandshakeContext handshakeContext = conContext.handshakeContext;
+ return handshakeContext == null
+ ? null
+ : handshakeContext.handshakeSession;
+ }
+
+ /**
+ * {@return the {@link AlgorithmConstraints} that are applicable for this engine,
+ * or null if none are applicable}
+ */
+ AlgorithmConstraints getAlgorithmConstraints() {
+ final HandshakeContext handshakeContext = conContext.handshakeContext;
+ // if we are handshaking then use the handshake context
+ // to determine the constraints, else use the configured
+ // SSLParameters
+ return handshakeContext == null
+ ? getSSLParameters().getAlgorithmConstraints()
+ : handshakeContext.sslConfig.userSpecifiedAlgorithmConstraints;
+ }
+
+ @Override
+ public SSLParameters getSSLParameters() {
+ return conContext.sslConfig.getSSLParameters();
+ }
+
+ @Override
+ public String getApplicationProtocol() {
+ // TODO: review thread safety when dealing with conContext
+ return conContext.applicationProtocol;
+ }
+
+ @Override
+ public Set getSupportedQuicVersions() {
+ return SUPPORTED_QUIC_VERSIONS;
+ }
+
+ @Override
+ public void setOneRttContext(final QuicOneRttContext ctx) {
+ this.oneRttKeyManager.setOneRttContext(ctx);
+ }
+
+ private QuicVersion getNegotiatedVersion() {
+ final QuicVersion negotiated = this.negotiatedVersion;
+ if (negotiated == null) {
+ throw new IllegalStateException(
+ "Quic version hasn't been negotiated yet");
+ }
+ return negotiated;
+ }
+
+ private boolean isEnabled(final QuicVersion quicVersion) {
+ final Set enabled = getSupportedQuicVersions();
+ if (enabled == null) {
+ return false;
+ }
+ return enabled.contains(quicVersion);
+ }
+
+ /**
+ * Returns the current handshake state of the connection. Sometimes packets
+ * that could be decrypted can be received before the handshake has
+ * completed, but should not be decrypted until it is complete
+ *
+ * @return the HandshakeState
+ */
+ @Override
+ public HandshakeState getHandshakeState() {
+ return handshakeState;
+ }
+
+ /**
+ * Returns the current sending key space (encryption level)
+ *
+ * @return the current sending key space
+ */
+ @Override
+ public KeySpace getCurrentSendKeySpace() {
+ return sendKeySpace;
+ }
+
+ @Override
+ public boolean keysAvailable(KeySpace keySpace) {
+ return switch (keySpace) {
+ case INITIAL -> this.initialKeyManager.keysAvailable();
+ case HANDSHAKE -> this.handshakeKeyManager.keysAvailable();
+ case ONE_RTT -> this.oneRttKeyManager.keysAvailable();
+ case ZERO_RTT -> false;
+ case RETRY -> true;
+ default -> throw new IllegalArgumentException(
+ keySpace + " not expected here");
+ };
+ }
+
+ @Override
+ public void discardKeys(KeySpace keySpace) {
+ switch (keySpace) {
+ case INITIAL -> this.initialKeyManager.discardKeys();
+ case HANDSHAKE -> this.handshakeKeyManager.discardKeys();
+ case ONE_RTT -> this.oneRttKeyManager.discardKeys();
+ default -> throw new IllegalArgumentException(
+ "key discarding not implemented for " + keySpace);
+ }
+ }
+
+ @Override
+ public int getHeaderProtectionSampleSize(KeySpace keySpace) {
+ return switch (keySpace) {
+ case INITIAL, HANDSHAKE, ZERO_RTT, ONE_RTT -> 16;
+ default -> throw new IllegalArgumentException(
+ "Type '" + keySpace + "' not expected here");
+ };
+ }
+
+ @Override
+ public ByteBuffer computeHeaderProtectionMask(KeySpace keySpace,
+ boolean incoming, ByteBuffer sample)
+ throws QuicKeyUnavailableException, QuicTransportException {
+ final QuicKeyManager keyManager = keyManager(keySpace);
+ if (incoming) {
+ final QuicCipher.QuicReadCipher quicCipher =
+ keyManager.getReadCipher();
+ return quicCipher.computeHeaderProtectionMask(sample);
+ } else {
+ final QuicCipher.QuicWriteCipher quicCipher =
+ keyManager.getWriteCipher();
+ return quicCipher.computeHeaderProtectionMask(sample);
+ }
+ }
+
+ @Override
+ public int getAuthTagSize() {
+ // RFC-9001, section 5.3
+ // QUIC can use any of the cipher suites defined in [TLS13] with the
+ // exception of TLS_AES_128_CCM_8_SHA256. ...
+ // These cipher suites have a 16-byte authentication tag and produce
+ // an output 16 bytes larger than their input.
+ return 16;
+ }
+
+ @Override
+ public void encryptPacket(final KeySpace keySpace, final long packetNumber,
+ final IntFunction headerGenerator,
+ final ByteBuffer packetPayload, final ByteBuffer output)
+ throws QuicKeyUnavailableException, QuicTransportException, ShortBufferException {
+ final QuicKeyManager keyManager = keyManager(keySpace);
+ keyManager.encryptPacket(packetNumber, headerGenerator, packetPayload, output);
+ }
+
+ @Override
+ public void decryptPacket(final KeySpace keySpace,
+ final long packetNumber, final int keyPhase,
+ final ByteBuffer packet, final int headerLength,
+ final ByteBuffer output)
+ throws QuicKeyUnavailableException, AEADBadTagException,
+ QuicTransportException, ShortBufferException {
+ if (keySpace == ONE_RTT && !isTLSHandshakeComplete()) {
+ // RFC-9001, section 5.7 specifies that the server or the client MUST NOT
+ // decrypt 1-RTT packets, even if 1-RTT keys are available, before the
+ // TLS handshake is complete.
+ throw new QuicKeyUnavailableException("QUIC TLS handshake not yet complete", ONE_RTT);
+ }
+ final QuicKeyManager keyManager = keyManager(keySpace);
+ keyManager.decryptPacket(packetNumber, keyPhase, packet, headerLength,
+ output);
+ }
+
+ @Override
+ public void signRetryPacket(final QuicVersion quicVersion,
+ final ByteBuffer originalConnectionId, final ByteBuffer packet,
+ final ByteBuffer output)
+ throws ShortBufferException, QuicTransportException {
+ if (!isEnabled(quicVersion)) {
+ throw new IllegalArgumentException(
+ "Quic version " + quicVersion + " isn't enabled");
+ }
+ int connIdLength = originalConnectionId.remaining();
+ if (connIdLength >= 256 || connIdLength < 0) {
+ throw new IllegalArgumentException("connection ID length");
+ }
+ final Cipher cipher = InitialKeyManager.getRetryCipher(
+ quicVersion, false);
+ cipher.updateAAD(new byte[]{(byte) connIdLength});
+ cipher.updateAAD(originalConnectionId);
+ cipher.updateAAD(packet);
+ try {
+ // No data to encrypt, just outputting the tag which will be
+ // verified later.
+ cipher.doFinal(ByteBuffer.allocate(0), output);
+ } catch (ShortBufferException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new QuicTransportException("Failed to sign packet",
+ null, 0, BASE_CRYPTO_ERROR + Alert.INTERNAL_ERROR.id, e);
+ }
+ }
+
+ @Override
+ public void verifyRetryPacket(final QuicVersion quicVersion,
+ final ByteBuffer originalConnectionId,
+ final ByteBuffer packet)
+ throws AEADBadTagException, QuicTransportException {
+ if (!isEnabled(quicVersion)) {
+ throw new IllegalArgumentException(
+ "Quic version " + quicVersion + " isn't enabled");
+ }
+ int connIdLength = originalConnectionId.remaining();
+ if (connIdLength >= 256 || connIdLength < 0) {
+ throw new IllegalArgumentException("connection ID length");
+ }
+ int originalLimit = packet.limit();
+ packet.limit(originalLimit - 16);
+ final Cipher cipher =
+ InitialKeyManager.getRetryCipher(quicVersion, true);
+ cipher.updateAAD(new byte[]{(byte) connIdLength});
+ cipher.updateAAD(originalConnectionId);
+ cipher.updateAAD(packet);
+ packet.limit(originalLimit);
+ try {
+ assert packet.remaining() == 16;
+ int outBufLength = cipher.getOutputSize(packet.remaining());
+ // No data to decrypt, just checking the tag.
+ ByteBuffer outBuffer = ByteBuffer.allocate(outBufLength);
+ cipher.doFinal(packet, outBuffer);
+ assert outBuffer.position() == 0;
+ } catch (AEADBadTagException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new QuicTransportException("Failed to verify packet",
+ null, 0, BASE_CRYPTO_ERROR + Alert.INTERNAL_ERROR.id, e);
+ }
+ }
+
+ private QuicKeyManager keyManager(final KeySpace keySpace) {
+ return switch (keySpace) {
+ case INITIAL -> this.initialKeyManager;
+ case HANDSHAKE -> this.handshakeKeyManager;
+ case ONE_RTT -> this.oneRttKeyManager;
+ default -> throw new IllegalArgumentException(
+ "No key manager available for key space: " + keySpace);
+ };
+ }
+
+ @Override
+ public ByteBuffer getHandshakeBytes(KeySpace keySpace) throws IOException {
+ if (keySpace != sendKeySpace) {
+ throw new IllegalStateException("Unexpected key space: "
+ + keySpace + " (expected " + sendKeySpace + ")");
+ }
+ if (handshakeState == HandshakeState.NEED_SEND_CRYPTO ||
+ !conContext.outputRecord.isEmpty()) { // session ticket
+ byte[] bytes = produceNextHandshakeMessage();
+ return ByteBuffer.wrap(bytes);
+ } else {
+ return null;
+ }
+ }
+
+ private byte[] produceNextHandshakeMessage() throws IOException {
+ if (!conContext.isNegotiated && !conContext.isBroken &&
+ !conContext.isInboundClosed() &&
+ !conContext.isOutboundClosed()) {
+ conContext.kickstart();
+ }
+ byte[] message = conContext.outputRecord.getHandshakeMessage();
+ if (handshakeState == NEED_SEND_CRYPTO) {
+ if (conContext.outputRecord.isEmpty()) {
+ if (conContext.isNegotiated) {
+ // client, done
+ handshakeState = NEED_RECV_HANDSHAKE_DONE;
+ sendKeySpace = ONE_RTT;
+ } else {
+ handshakeState = NEED_RECV_CRYPTO;
+ }
+ } else if (sendKeySpace == INITIAL && !getUseClientMode()) {
+ // Server sends handshake messages immediately after
+ // the initial server hello. Need to check the next key space.
+ sendKeySpace = conContext.outputRecord.getHandshakeMessageKeySpace();
+ }
+ } else {
+ assert conContext.isNegotiated;
+ }
+ return message;
+ }
+
+ @Override
+ public void consumeHandshakeBytes(KeySpace keySpace, ByteBuffer payload)
+ throws QuicTransportException {
+ if (!payload.hasRemaining()) {
+ throw new IllegalArgumentException("Empty crypto buffer");
+ }
+ if (keySpace == KeySpace.ZERO_RTT) {
+ throw new IllegalArgumentException("Crypto in zero-rtt");
+ }
+ if (incomingCryptoSpace != null && incomingCryptoSpace != keySpace) {
+ throw new QuicTransportException("Unexpected message", null, 0,
+ BASE_CRYPTO_ERROR + Alert.UNEXPECTED_MESSAGE.id,
+ new SSLHandshakeException(
+ "Unfinished message in " + incomingCryptoSpace));
+ }
+ try {
+ if (!conContext.isNegotiated && !conContext.isBroken &&
+ !conContext.isInboundClosed() &&
+ !conContext.isOutboundClosed()) {
+ conContext.kickstart();
+ }
+ } catch (IOException e) {
+ throw new QuicTransportException(e.toString(), null, 0,
+ BASE_CRYPTO_ERROR + Alert.INTERNAL_ERROR.id, e);
+ }
+ // previously unconsumed bytes in incomingCryptoBuffer, new bytes in
+ // payload. if incomingCryptoBuffer is not null, it's either 4 bytes
+ // or large enough to hold the entire message.
+ while (payload.hasRemaining()) {
+ if (keySpace != KeySpace.ONE_RTT &&
+ handshakeState != HandshakeState.NEED_RECV_CRYPTO) {
+ // in one-rtt we may receive session tickets at any time;
+ // during handshake we're either sending or receiving
+ throw new QuicTransportException("Unexpected message", null, 0,
+ BASE_CRYPTO_ERROR + Alert.UNEXPECTED_MESSAGE.id,
+ new SSLHandshakeException(
+ "Not expecting a handshake message, state: " +
+ handshakeState));
+ }
+ if (incomingCryptoBuffer != null) {
+ // message type validated already; pump more bytes
+ if (payload.remaining() <= incomingCryptoBuffer.remaining()) {
+ incomingCryptoBuffer.put(payload);
+ } else {
+ // more than one message in buffer, or we don't have a
+ // header yet
+ int remaining = incomingCryptoBuffer.remaining();
+ incomingCryptoBuffer.put(incomingCryptoBuffer.position(),
+ payload, payload.position(), remaining);
+ incomingCryptoBuffer.position(incomingCryptoBuffer.limit());
+ payload.position(payload.position() + remaining);
+ if (incomingCryptoBuffer.capacity() == 4) {
+ // small buffer for header only; retrieve size and
+ // expand if necessary
+ int messageSize =
+ ((incomingCryptoBuffer.get(1) & 0xFF) << 16) |
+ ((incomingCryptoBuffer.get(2) & 0xFF) << 8) |
+ (incomingCryptoBuffer.get(3) & 0xFF);
+ if (messageSize != 0) {
+ if (messageSize > SSLConfiguration.maxHandshakeMessageSize) {
+ throw new QuicTransportException(
+ "The size of the handshake message ("
+ + messageSize
+ + ") exceeds the maximum allowed size ("
+ + SSLConfiguration.maxHandshakeMessageSize
+ + ")",
+ null, 0,
+ QuicTransportErrors.CRYPTO_BUFFER_EXCEEDED);
+ }
+ ByteBuffer newBuffer =
+ ByteBuffer.allocate(messageSize + 4);
+ incomingCryptoBuffer.flip();
+ newBuffer.put(incomingCryptoBuffer);
+ incomingCryptoBuffer = newBuffer;
+ assert incomingCryptoBuffer.position() == 4 :
+ incomingCryptoBuffer.position();
+ // start over with larger buffer
+ continue;
+ }
+ // message size was zero... can it really happen?
+ }
+ }
+ } else {
+ // incoming crypto buffer is null. Validate message type,
+ // check if size is available
+ byte messageType = payload.get(payload.position());
+ if (SSLLogger.isOn) {
+ SSLLogger.fine("Received message of type 0x" +
+ Integer.toHexString(messageType & 0xFF));
+ }
+ KeySpace expected = messageTypeMap.get(messageType);
+ if (expected != keySpace) {
+ throw new QuicTransportException("Unexpected message",
+ null, 0,
+ BASE_CRYPTO_ERROR + Alert.UNEXPECTED_MESSAGE.id,
+ new SSLHandshakeException("Message " + messageType +
+ " received in " + keySpace +
+ " but should be " + expected));
+ }
+ if (payload.remaining() < 4) {
+ // partial message, length missing. Store in
+ // incomingCryptoBuffer
+ incomingCryptoBuffer = ByteBuffer.allocate(4);
+ incomingCryptoBuffer.put(payload);
+ incomingCryptoSpace = keySpace;
+ return;
+ }
+ int payloadPos = payload.position();
+ int messageSize = ((payload.get(payloadPos + 1) & 0xFF) << 16)
+ | ((payload.get(payloadPos + 2) & 0xFF) << 8)
+ | (payload.get(payloadPos + 3) & 0xFF);
+ if (payload.remaining() < messageSize + 4) {
+ // partial message, length known. Store in
+ // incomingCryptoBuffer
+ if (messageSize > SSLConfiguration.maxHandshakeMessageSize) {
+ throw new QuicTransportException(
+ "The size of the handshake message ("
+ + messageSize
+ + ") exceeds the maximum allowed size ("
+ + SSLConfiguration.maxHandshakeMessageSize
+ + ")",
+ null, 0,
+ QuicTransportErrors.CRYPTO_BUFFER_EXCEEDED);
+ }
+ incomingCryptoBuffer = ByteBuffer.allocate(messageSize + 4);
+ incomingCryptoBuffer.put(payload);
+ incomingCryptoSpace = keySpace;
+ return;
+ }
+ incomingCryptoSpace = keySpace;
+ incomingCryptoBuffer = payload.slice(payloadPos,
+ messageSize + 4);
+ // set position at end to indicate that the buffer is ready
+ // for processing
+ incomingCryptoBuffer.position(messageSize + 4);
+ assert !incomingCryptoBuffer.hasRemaining() :
+ incomingCryptoBuffer.remaining();
+ payload.position(payloadPos + messageSize + 4);
+ }
+ if (!incomingCryptoBuffer.hasRemaining()) {
+ incomingCryptoBuffer.flip();
+ handleHandshakeMessage(keySpace, incomingCryptoBuffer);
+ incomingCryptoBuffer = null;
+ incomingCryptoSpace = null;
+ } else {
+ assert !payload.hasRemaining() : payload.remaining();
+ return;
+ }
+ }
+ }
+
+ private void handleHandshakeMessage(KeySpace keySpace, ByteBuffer message)
+ throws QuicTransportException {
+ // message param contains one whole TLS message
+ boolean useClientMode = getUseClientMode();
+ byte messageType = message.get();
+ int messageSize = ((message.get() & 0xFF) << 16)
+ | ((message.get() & 0xFF) << 8)
+ | (message.get() & 0xFF);
+
+ assert message.remaining() == messageSize :
+ message.remaining() - messageSize;
+ try {
+ if (conContext.inputRecord.handshakeHash.isHashable(messageType)) {
+ ByteBuffer temp = message.duplicate();
+ temp.position(0);
+ conContext.inputRecord.handshakeHash.receive(temp);
+ }
+ if (conContext.handshakeContext == null) {
+ if (!conContext.isNegotiated) {
+ throw new QuicTransportException(
+ "Cannot process crypto message, broken: "
+ + conContext.isBroken,
+ null, 0, QuicTransportErrors.INTERNAL_ERROR);
+ }
+ conContext.handshakeContext =
+ new PostHandshakeContext(conContext);
+ }
+ conContext.handshakeContext.dispatch(messageType, message.slice());
+ } catch (SSLHandshakeException e) {
+ if (e.getCause() instanceof QuicTransportException qte) {
+ // rethrow quic transport parameters validation exception
+ throw qte;
+ }
+ Alert alert = ((QuicEngineOutputRecord)
+ conContext.outputRecord).getAlert();
+ throw new QuicTransportException(alert.description, keySpace, 0,
+ BASE_CRYPTO_ERROR + alert.id, e);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ if (handshakeState == NEED_RECV_CRYPTO) {
+ if (conContext.outputRecord.isEmpty()) {
+ if (conContext.isNegotiated) {
+ // dead code? done, server side, no session ticket
+ handshakeState = NEED_SEND_HANDSHAKE_DONE;
+ sendKeySpace = ONE_RTT;
+ } else {
+ // expect more messages
+ // client side: if we're still in INITIAL, switch
+ // to HANDSHAKE
+ if (sendKeySpace == INITIAL) {
+ sendKeySpace = HANDSHAKE;
+ }
+ }
+ } else {
+ // our turn to send
+ if (conContext.isNegotiated && !useClientMode) {
+ // done, server side, wants to send session ticket
+ handshakeState = NEED_SEND_HANDSHAKE_DONE;
+ sendKeySpace = ONE_RTT;
+ } else {
+ // more messages needed to finish handshake
+ handshakeState = HandshakeState.NEED_SEND_CRYPTO;
+ }
+ }
+ } else {
+ assert conContext.isNegotiated;
+ }
+ }
+
+ @Override
+ public void deriveInitialKeys(final QuicVersion quicVersion,
+ final ByteBuffer connectionId) throws IOException {
+ if (!isEnabled(quicVersion)) {
+ throw new IllegalArgumentException("Quic version " + quicVersion +
+ " isn't enabled");
+ }
+ final byte[] connectionIdBytes = new byte[connectionId.remaining()];
+ connectionId.get(connectionIdBytes);
+ this.initialKeyManager.deriveKeys(quicVersion, connectionIdBytes,
+ getUseClientMode());
+ }
+
+ @Override
+ public void versionNegotiated(final QuicVersion quicVersion) {
+ Objects.requireNonNull(quicVersion);
+ if (!isEnabled(quicVersion)) {
+ throw new IllegalArgumentException("Quic version " + quicVersion +
+ " is not enabled");
+ }
+ synchronized (this) {
+ final QuicVersion prevNegotiated = this.negotiatedVersion;
+ if (prevNegotiated != null) {
+ throw new IllegalStateException("A Quic version has already " +
+ "been negotiated previously");
+ }
+ this.negotiatedVersion = quicVersion;
+ }
+ }
+
+ public void deriveHandshakeKeys() throws IOException {
+ final QuicVersion quicVersion = getNegotiatedVersion();
+ this.handshakeKeyManager.deriveKeys(quicVersion,
+ this.conContext.handshakeContext,
+ getUseClientMode());
+ }
+
+ public void deriveOneRTTKeys() throws IOException {
+ final QuicVersion quicVersion = getNegotiatedVersion();
+ this.oneRttKeyManager.deriveKeys(quicVersion,
+ this.conContext.handshakeContext,
+ getUseClientMode());
+ }
+
+ // for testing (PacketEncryptionTest)
+ void deriveOneRTTKeys(final QuicVersion version,
+ final SecretKey client_application_traffic_secret_0,
+ final SecretKey server_application_traffic_secret_0,
+ final CipherSuite negotiatedCipherSuite,
+ final boolean clientMode) throws IOException,
+ GeneralSecurityException {
+ this.oneRttKeyManager.deriveOneRttKeys(version,
+ client_application_traffic_secret_0,
+ server_application_traffic_secret_0,
+ negotiatedCipherSuite, clientMode);
+ }
+
+ @Override
+ public Runnable getDelegatedTask() {
+ // TODO: actually delegate tasks
+ return null;
+ }
+
+ @Override
+ public String getPeerHost() {
+ return peerHost;
+ }
+
+ @Override
+ public int getPeerPort() {
+ return peerPort;
+ }
+
+ @Override
+ public boolean useDelegatedTask() {
+ return true;
+ }
+
+ public byte[] getLocalQuicTransportParameters() {
+ ByteBuffer ltp = localQuicTransportParameters;
+ if (ltp == null) {
+ return null;
+ }
+ byte[] result = new byte[ltp.remaining()];
+ ltp.get(0, result);
+ return result;
+ }
+
+ @Override
+ public void setLocalQuicTransportParameters(ByteBuffer params) {
+ this.localQuicTransportParameters = params;
+ }
+
+ @Override
+ public void restartHandshake() throws IOException {
+ if (negotiatedVersion != null) {
+ throw new IllegalStateException("Version already negotiated");
+ }
+ if (sendKeySpace != INITIAL || handshakeState != NEED_RECV_CRYPTO) {
+ throw new IllegalStateException("Unexpected handshake state");
+ }
+ HandshakeContext context = conContext.handshakeContext;
+ ClientHandshakeContext chc = (ClientHandshakeContext)context;
+
+ // Refresh handshake hash
+ chc.handshakeHash.finish(); // reset the handshake hash
+
+ // Update the initial ClientHello handshake message.
+ chc.initialClientHelloMsg.extensions.reproduce(chc,
+ new SSLExtension[] {
+ SSLExtension.CH_QUIC_TRANSPORT_PARAMETERS,
+ SSLExtension.CH_PRE_SHARED_KEY
+ });
+
+ // produce handshake message
+ chc.initialClientHelloMsg.write(chc.handshakeOutput);
+ handshakeState = NEED_SEND_CRYPTO;
+ }
+
+ @Override
+ public void setRemoteQuicTransportParametersConsumer(
+ QuicTransportParametersConsumer consumer) {
+ this.remoteQuicTransportParametersConsumer = consumer;
+ }
+
+ void processRemoteQuicTransportParameters(ByteBuffer buffer)
+ throws QuicTransportException{
+ remoteQuicTransportParametersConsumer.accept(buffer);
+ }
+
+ @Override
+ public boolean tryMarkHandshakeDone() {
+ if (getUseClientMode()) {
+ // not expected to be called on client
+ throw new IllegalStateException(
+ "Not expected to be called in client mode");
+ }
+ final boolean confirmed = HANDSHAKE_STATE_HANDLE.compareAndSet(this,
+ NEED_SEND_HANDSHAKE_DONE, HANDSHAKE_CONFIRMED);
+ if (confirmed) {
+ if (SSLLogger.isOn) {
+ SSLLogger.fine("QuicTLSEngine (server) marked handshake " +
+ "state as HANDSHAKE_CONFIRMED");
+ }
+ }
+ return confirmed;
+ }
+
+ @Override
+ public boolean tryReceiveHandshakeDone() {
+ final boolean isClient = getUseClientMode();
+ if (!isClient) {
+ throw new IllegalStateException(
+ "Not expected to receive HANDSHAKE_DONE in server mode");
+ }
+ final boolean confirmed = HANDSHAKE_STATE_HANDLE.compareAndSet(this,
+ NEED_RECV_HANDSHAKE_DONE, HANDSHAKE_CONFIRMED);
+ if (confirmed) {
+ if (SSLLogger.isOn) {
+ SSLLogger.fine(
+ "QuicTLSEngine (client) received HANDSHAKE_DONE," +
+ " marking state as HANDSHAKE_DONE");
+ }
+ }
+ return confirmed;
+ }
+
+ @Override
+ public boolean isTLSHandshakeComplete() {
+ final boolean isClient = getUseClientMode();
+ final HandshakeState hsState = this.handshakeState;
+ if (isClient) {
+ // the client has received TLS Finished message from server and
+ // has sent its own TLS Finished message and is waiting for the server
+ // to send QUIC HANDSHAKE_DONE frame.
+ // OR
+ // the client has received TLS Finished message from server and
+ // has sent its own TLS Finished message and has even received the
+ // QUIC HANDSHAKE_DONE frame.
+ // Either of these implies the TLS handshake is complete for the client
+ return hsState == NEED_RECV_HANDSHAKE_DONE || hsState == HANDSHAKE_CONFIRMED;
+ }
+ // on the server side the TLS handshake is complete only when the server has
+ // sent a TLS Finished message and received the client's Finished message.
+ return hsState == HANDSHAKE_CONFIRMED;
+ }
+
+ /**
+ * {@return the key phase being used when decrypting incoming 1-RTT
+ * packets}
+ */
+ // this is only used in tests
+ public int getOneRttKeyPhase() throws QuicKeyUnavailableException {
+ return this.oneRttKeyManager.getReadCipher().getKeyPhase();
+ }
+}
diff --git a/src/java.base/share/classes/sun/security/ssl/QuicTransportParametersExtension.java b/src/java.base/share/classes/sun/security/ssl/QuicTransportParametersExtension.java
new file mode 100644
index 00000000000..83e977ee446
--- /dev/null
+++ b/src/java.base/share/classes/sun/security/ssl/QuicTransportParametersExtension.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (c) 2022, 2025, 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. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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 sun.security.ssl;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+import jdk.internal.net.quic.QuicTransportException;
+import sun.security.ssl.SSLExtension.ExtensionConsumer;
+import sun.security.ssl.SSLHandshake.HandshakeMessage;
+
+/**
+ * Pack of the "quic_transport_parameters" extensions [RFC 9001].
+ */
+final class QuicTransportParametersExtension {
+
+ static final HandshakeProducer chNetworkProducer =
+ new T13CHQuicParametersProducer();
+ static final ExtensionConsumer chOnLoadConsumer =
+ new T13CHQuicParametersConsumer();
+ static final HandshakeAbsence chOnLoadAbsence =
+ new T13CHQuicParametersAbsence();
+ static final HandshakeProducer eeNetworkProducer =
+ new T13EEQuicParametersProducer();
+ static final ExtensionConsumer eeOnLoadConsumer =
+ new T13EEQuicParametersConsumer();
+ static final HandshakeAbsence eeOnLoadAbsence =
+ new T13EEQuicParametersAbsence();
+
+ private static final class T13CHQuicParametersProducer
+ implements HandshakeProducer {
+ // Prevent instantiation of this class.
+ private T13CHQuicParametersProducer() {
+ }
+
+ @Override
+ public byte[] produce(ConnectionContext context,
+ HandshakeMessage message) throws IOException {
+
+ ClientHandshakeContext chc = (ClientHandshakeContext) context;
+ if (!chc.sslConfig.isQuic) {
+ return null;
+ }
+ QuicTLSEngineImpl quicTLSEngine =
+ (QuicTLSEngineImpl) chc.conContext.transport;
+
+ return quicTLSEngine.getLocalQuicTransportParameters();
+ }
+
+ }
+
+ private static final class T13CHQuicParametersConsumer
+ implements ExtensionConsumer {
+ // Prevent instantiation of this class.
+ private T13CHQuicParametersConsumer() {
+ }
+
+ @Override
+ public void consume(ConnectionContext context,
+ HandshakeMessage message, ByteBuffer buffer)
+ throws IOException {
+ ServerHandshakeContext shc = (ServerHandshakeContext) context;
+ if (!shc.sslConfig.isQuic) {
+ throw shc.conContext.fatal(Alert.UNSUPPORTED_EXTENSION,
+ "Client sent the quic_transport_parameters " +
+ "extension in a non-QUIC context");
+ }
+ QuicTLSEngineImpl quicTLSEngine =
+ (QuicTLSEngineImpl) shc.conContext.transport;
+ try {
+ quicTLSEngine.processRemoteQuicTransportParameters(buffer);
+ } catch (QuicTransportException e) {
+ throw shc.conContext.fatal(Alert.DECODE_ERROR, e);
+ }
+
+ }
+ }
+
+ private static final class T13CHQuicParametersAbsence
+ implements HandshakeAbsence {
+ // Prevent instantiation of this class.
+ private T13CHQuicParametersAbsence() {
+ }
+
+ @Override
+ public void absent(ConnectionContext context,
+ HandshakeMessage message) throws IOException {
+ // The producing happens in server side only.
+ ServerHandshakeContext shc = (ServerHandshakeContext)context;
+
+ if (shc.sslConfig.isQuic) {
+ // RFC 9001: endpoints MUST send quic_transport_parameters
+ throw shc.conContext.fatal(
+ Alert.MISSING_EXTENSION,
+ "Client did not send QUIC transport parameters");
+ }
+ }
+ }
+
+ private static final class T13EEQuicParametersProducer
+ implements HandshakeProducer {
+ // Prevent instantiation of this class.
+ private T13EEQuicParametersProducer() {
+ }
+
+ @Override
+ public byte[] produce(ConnectionContext context,
+ HandshakeMessage message) {
+
+ ServerHandshakeContext shc = (ServerHandshakeContext) context;
+ if (!shc.sslConfig.isQuic) {
+ return null;
+ }
+ QuicTLSEngineImpl quicTLSEngine =
+ (QuicTLSEngineImpl) shc.conContext.transport;
+
+ return quicTLSEngine.getLocalQuicTransportParameters();
+ }
+ }
+
+ private static final class T13EEQuicParametersConsumer
+ implements ExtensionConsumer {
+ // Prevent instantiation of this class.
+ private T13EEQuicParametersConsumer() {
+ }
+
+ @Override
+ public void consume(ConnectionContext context,
+ HandshakeMessage message, ByteBuffer buffer)
+ throws IOException {
+ ClientHandshakeContext chc = (ClientHandshakeContext) context;
+ if (!chc.sslConfig.isQuic) {
+ throw chc.conContext.fatal(Alert.UNSUPPORTED_EXTENSION,
+ "Server sent the quic_transport_parameters " +
+ "extension in a non-QUIC context");
+ }
+ QuicTLSEngineImpl quicTLSEngine =
+ (QuicTLSEngineImpl) chc.conContext.transport;
+ try {
+ quicTLSEngine.processRemoteQuicTransportParameters(buffer);
+ } catch (QuicTransportException e) {
+ throw chc.conContext.fatal(Alert.DECODE_ERROR, e);
+ }
+ }
+ }
+
+ private static final class T13EEQuicParametersAbsence
+ implements HandshakeAbsence {
+ // Prevent instantiation of this class.
+ private T13EEQuicParametersAbsence() {
+ }
+
+ @Override
+ public void absent(ConnectionContext context,
+ HandshakeMessage message) throws IOException {
+ ClientHandshakeContext chc = (ClientHandshakeContext) context;
+
+ if (chc.sslConfig.isQuic) {
+ // RFC 9001: endpoints MUST send quic_transport_parameters
+ throw chc.conContext.fatal(
+ Alert.MISSING_EXTENSION,
+ "Server did not send QUIC transport parameters");
+ }
+ }
+ }
+}
diff --git a/src/java.base/share/classes/sun/security/ssl/SSLAlgorithmConstraints.java b/src/java.base/share/classes/sun/security/ssl/SSLAlgorithmConstraints.java
index 95cfc6082be..1d5a4c4e73d 100644
--- a/src/java.base/share/classes/sun/security/ssl/SSLAlgorithmConstraints.java
+++ b/src/java.base/share/classes/sun/security/ssl/SSLAlgorithmConstraints.java
@@ -37,6 +37,8 @@ import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import javax.net.ssl.*;
+
+import jdk.internal.net.quic.QuicTLSEngine;
import sun.security.util.DisabledAlgorithmConstraints;
import static sun.security.util.DisabledAlgorithmConstraints.*;
@@ -162,6 +164,33 @@ final class SSLAlgorithmConstraints implements AlgorithmConstraints {
withDefaultCertPathConstraints);
}
+ /**
+ * Returns an {@link AlgorithmConstraints} instance that uses the
+ * constraints configured for the given {@code engine} in addition
+ * to the platform configured constraints.
+ *
+ * If the given {@code allowedAlgorithms} is non-null then the returned
+ * {@code AlgorithmConstraints} will only permit those allowed algorithms.
+ *
+ * @param engine QuicTLSEngine used to determine the constraints
+ * @param mode SIGNATURE_CONSTRAINTS_MODE
+ * @param withDefaultCertPathConstraints whether or not to apply the default certpath
+ * algorithm constraints too
+ * @return a AlgorithmConstraints instance
+ */
+ static AlgorithmConstraints forQUIC(QuicTLSEngine engine,
+ SIGNATURE_CONSTRAINTS_MODE mode,
+ boolean withDefaultCertPathConstraints) {
+ if (engine == null) {
+ return wrap(null, withDefaultCertPathConstraints);
+ }
+
+ return new SSLAlgorithmConstraints(
+ nullIfDefault(getUserSpecifiedConstraints(engine)),
+ new SupportedSignatureAlgorithmConstraints(engine.getHandshakeSession(), mode),
+ withDefaultCertPathConstraints);
+ }
+
private static AlgorithmConstraints nullIfDefault(
AlgorithmConstraints constraints) {
return constraints == DEFAULT ? null : constraints;
@@ -207,6 +236,17 @@ final class SSLAlgorithmConstraints implements AlgorithmConstraints {
return null;
}
+ private static AlgorithmConstraints getUserSpecifiedConstraints(
+ QuicTLSEngine quicEngine) {
+ if (quicEngine != null) {
+ if (quicEngine instanceof QuicTLSEngineImpl engineImpl) {
+ return engineImpl.getAlgorithmConstraints();
+ }
+ return quicEngine.getSSLParameters().getAlgorithmConstraints();
+ }
+ return null;
+ }
+
@Override
public boolean permits(Set primitives,
String algorithm, AlgorithmParameters parameters) {
diff --git a/src/java.base/share/classes/sun/security/ssl/SSLConfiguration.java b/src/java.base/share/classes/sun/security/ssl/SSLConfiguration.java
index bb032e019d3..aacac465027 100644
--- a/src/java.base/share/classes/sun/security/ssl/SSLConfiguration.java
+++ b/src/java.base/share/classes/sun/security/ssl/SSLConfiguration.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2018, 2025, 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
@@ -78,6 +78,7 @@ final class SSLConfiguration implements Cloneable {
boolean noSniExtension;
boolean noSniMatcher;
+ boolean isQuic;
// To switch off the extended_master_secret extension.
static final boolean useExtendedMasterSecret;
@@ -91,7 +92,7 @@ final class SSLConfiguration implements Cloneable {
Utilities.getBooleanProperty("jdk.tls.allowLegacyMasterSecret", true);
// Use TLS1.3 middlebox compatibility mode.
- static final boolean useCompatibilityMode = Utilities.getBooleanProperty(
+ private static final boolean useCompatibilityMode = Utilities.getBooleanProperty(
"jdk.tls.client.useCompatibilityMode", true);
// Respond a close_notify alert if receiving close_notify alert.
@@ -524,6 +525,14 @@ final class SSLConfiguration implements Cloneable {
}
}
+ public boolean isUseCompatibilityMode() {
+ return useCompatibilityMode && !isQuic;
+ }
+
+ public void setQuic(boolean quic) {
+ isQuic = quic;
+ }
+
@Override
@SuppressWarnings({"unchecked", "CloneDeclaresCloneNotSupported"})
public Object clone() {
@@ -567,7 +576,10 @@ final class SSLConfiguration implements Cloneable {
*/
private static String[] getCustomizedSignatureScheme(String propertyName) {
String property = System.getProperty(propertyName);
- if (SSLLogger.isOn && SSLLogger.isOn("ssl,sslctx")) {
+ // this method is called from class initializer; logging here
+ // will occasionally pin threads and deadlock if called from a virtual thread
+ if (SSLLogger.isOn && SSLLogger.isOn("ssl,sslctx")
+ && !Thread.currentThread().isVirtual()) {
SSLLogger.fine(
"System property " + propertyName + " is set to '" +
property + "'");
@@ -595,7 +607,8 @@ final class SSLConfiguration implements Cloneable {
if (scheme != null && scheme.isAvailable) {
signatureSchemes.add(schemeName);
} else {
- if (SSLLogger.isOn && SSLLogger.isOn("ssl,sslctx")) {
+ if (SSLLogger.isOn && SSLLogger.isOn("ssl,sslctx")
+ && !Thread.currentThread().isVirtual()) {
SSLLogger.fine(
"The current installed providers do not " +
"support signature scheme: " + schemeName);
diff --git a/src/java.base/share/classes/sun/security/ssl/SSLContextImpl.java b/src/java.base/share/classes/sun/security/ssl/SSLContextImpl.java
index a0cb28201e9..85dde5b0dbb 100644
--- a/src/java.base/share/classes/sun/security/ssl/SSLContextImpl.java
+++ b/src/java.base/share/classes/sun/security/ssl/SSLContextImpl.java
@@ -481,6 +481,10 @@ public abstract class SSLContextImpl extends SSLContextSpi {
return availableProtocols;
}
+ public boolean isUsableWithQuic() {
+ return trustManager instanceof X509TrustManagerImpl;
+ }
+
/*
* The SSLContext implementation for SSL/(D)TLS algorithm
*
diff --git a/src/java.base/share/classes/sun/security/ssl/SSLExtension.java b/src/java.base/share/classes/sun/security/ssl/SSLExtension.java
index c7175ea7fdc..082914b4b4b 100644
--- a/src/java.base/share/classes/sun/security/ssl/SSLExtension.java
+++ b/src/java.base/share/classes/sun/security/ssl/SSLExtension.java
@@ -458,6 +458,28 @@ enum SSLExtension implements SSLStringizer {
null, null, null, null,
KeyShareExtension.hrrStringizer),
+ // Extension defined in RFC 9001
+ CH_QUIC_TRANSPORT_PARAMETERS (0x0039, "quic_transport_parameters",
+ SSLHandshake.CLIENT_HELLO,
+ ProtocolVersion.PROTOCOLS_OF_13,
+ QuicTransportParametersExtension.chNetworkProducer,
+ QuicTransportParametersExtension.chOnLoadConsumer,
+ QuicTransportParametersExtension.chOnLoadAbsence,
+ null,
+ null,
+ // TODO properly stringize, rather than hex output.
+ null),
+ EE_QUIC_TRANSPORT_PARAMETERS (0x0039, "quic_transport_parameters",
+ SSLHandshake.ENCRYPTED_EXTENSIONS,
+ ProtocolVersion.PROTOCOLS_OF_13,
+ QuicTransportParametersExtension.eeNetworkProducer,
+ QuicTransportParametersExtension.eeOnLoadConsumer,
+ QuicTransportParametersExtension.eeOnLoadAbsence,
+ null,
+ null,
+ // TODO properly stringize, rather than hex output
+ null),
+
// Extensions defined in RFC 5746 (TLS Renegotiation Indication Extension)
CH_RENEGOTIATION_INFO (0xff01, "renegotiation_info",
SSLHandshake.CLIENT_HELLO,
@@ -820,7 +842,10 @@ enum SSLExtension implements SSLStringizer {
private static Collection getDisabledExtensions(
String propertyName) {
String property = System.getProperty(propertyName);
- if (SSLLogger.isOn && SSLLogger.isOn("ssl,sslctx")) {
+ // this method is called from class initializer; logging here
+ // will occasionally pin threads and deadlock if called from a virtual thread
+ if (SSLLogger.isOn && SSLLogger.isOn("ssl,sslctx")
+ && !Thread.currentThread().isVirtual()) {
SSLLogger.fine(
"System property " + propertyName + " is set to '" +
property + "'");
diff --git a/src/java.base/share/classes/sun/security/ssl/ServerHello.java b/src/java.base/share/classes/sun/security/ssl/ServerHello.java
index d092d6c07de..1d2faa5351f 100644
--- a/src/java.base/share/classes/sun/security/ssl/ServerHello.java
+++ b/src/java.base/share/classes/sun/security/ssl/ServerHello.java
@@ -235,7 +235,8 @@ final class ServerHello {
serverVersion.name,
Utilities.toHexString(serverRandom.randomBytes),
sessionId.toString(),
- cipherSuite.name + "(" + Utilities.byte16HexString(cipherSuite.id) + ")",
+ cipherSuite.name +
+ "(" + Utilities.byte16HexString(cipherSuite.id) + ")",
HexFormat.of().toHexDigits(compressionMethod),
Utilities.indent(extensions.toString(), " ")
};
@@ -534,8 +535,9 @@ final class ServerHello {
// consider the handshake extension impact
SSLExtension[] enabledExtensions =
- shc.sslConfig.getEnabledExtensions(
- SSLHandshake.CLIENT_HELLO, shc.negotiatedProtocol);
+ shc.sslConfig.getEnabledExtensions(
+ SSLHandshake.CLIENT_HELLO,
+ shc.negotiatedProtocol);
clientHello.extensions.consumeOnTrade(shc, enabledExtensions);
shc.negotiatedProtocol =
@@ -670,6 +672,17 @@ final class ServerHello {
// Update the context for master key derivation.
shc.handshakeKeyDerivation = kd;
+ if (shc.sslConfig.isQuic) {
+ QuicTLSEngineImpl engine =
+ (QuicTLSEngineImpl) shc.conContext.transport;
+ try {
+ engine.deriveHandshakeKeys();
+ } catch (IOException e) {
+ // unlikely
+ throw shc.conContext.fatal(Alert.HANDSHAKE_FAILURE,
+ "Failed to derive keys", e);
+ }
+ }
// Check if the server supports stateless resumption
if (sessionCache.statelessEnabled()) {
shc.statelessResumption = true;
@@ -784,9 +797,9 @@ final class ServerHello {
// first handshake message. This may either be after
// a ServerHello or a HelloRetryRequest.
// (RFC 8446, Appendix D.4)
- shc.conContext.outputRecord.changeWriteCiphers(
- SSLWriteCipher.nullTlsWriteCipher(),
- (clientHello.sessionId.length() != 0));
+ if (clientHello.sessionId.length() != 0) {
+ shc.conContext.outputRecord.encodeChangeCipherSpec();
+ }
// Stateless, shall we clean up the handshake context as well?
shc.handshakeHash.finish(); // forgot about the handshake hash
@@ -1366,10 +1379,21 @@ final class ServerHello {
// Should use resumption_master_secret for TLS 1.3.
// chc.handshakeSession.setMasterSecret(masterSecret);
-
// Update the context for master key derivation.
chc.handshakeKeyDerivation = secretKD;
+ if (chc.sslConfig.isQuic) {
+ QuicTLSEngineImpl engine =
+ (QuicTLSEngineImpl) chc.conContext.transport;
+ try {
+ engine.deriveHandshakeKeys();
+ } catch (IOException e) {
+ // unlikely
+ throw chc.conContext.fatal(Alert.HANDSHAKE_FAILURE,
+ "Failed to derive keys", e);
+ }
+ }
+
// update the consumers and producers
//
// The server sends a dummy change_cipher_spec record immediately
diff --git a/src/java.base/share/classes/sun/security/ssl/SunX509KeyManagerImpl.java b/src/java.base/share/classes/sun/security/ssl/SunX509KeyManagerImpl.java
index 2441ad91fde..6bf138f4e45 100644
--- a/src/java.base/share/classes/sun/security/ssl/SunX509KeyManagerImpl.java
+++ b/src/java.base/share/classes/sun/security/ssl/SunX509KeyManagerImpl.java
@@ -195,6 +195,13 @@ final class SunX509KeyManagerImpl extends X509KeyManagerCertChecking {
getAlgorithmConstraints(engine), null, null);
}
+ @Override
+ String chooseQuicClientAlias(String[] keyTypes, Principal[] issuers,
+ QuicTLSEngineImpl quicTLSEngine) {
+ return chooseAlias(getKeyTypes(keyTypes), issuers, CheckType.CLIENT,
+ getAlgorithmConstraints(quicTLSEngine), null, null);
+ }
+
/*
* Choose an alias to authenticate the server side of a secure
* socket given the public key type and the list of
@@ -222,6 +229,16 @@ final class SunX509KeyManagerImpl extends X509KeyManagerCertChecking {
X509TrustManagerImpl.getRequestedServerNames(engine), "HTTPS");
}
+ @Override
+ String chooseQuicServerAlias(String keyType,
+ X500Principal[] issuers,
+ QuicTLSEngineImpl quicTLSEngine) {
+ return chooseAlias(getKeyTypes(keyType), issuers, CheckType.SERVER,
+ getAlgorithmConstraints(quicTLSEngine),
+ X509TrustManagerImpl.getRequestedServerNames(quicTLSEngine),
+ "HTTPS");
+ }
+
/*
* Get the matching aliases for authenticating the client side of a secure
* socket given the public key type and the list of
diff --git a/src/java.base/share/classes/sun/security/ssl/TransportContext.java b/src/java.base/share/classes/sun/security/ssl/TransportContext.java
index 717c81723ff..49fd664e9ed 100644
--- a/src/java.base/share/classes/sun/security/ssl/TransportContext.java
+++ b/src/java.base/share/classes/sun/security/ssl/TransportContext.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2018, 2025, 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
@@ -489,6 +489,10 @@ final class TransportContext implements ConnectionContext {
isUnsureMode = false;
}
+ public void setQuic(boolean quic) {
+ sslConfig.setQuic(quic);
+ }
+
// The OutputRecord is closed and not buffered output record.
boolean isOutboundDone() {
return outputRecord.isClosed() && outputRecord.isEmpty();
diff --git a/src/java.base/share/classes/sun/security/ssl/X509Authentication.java b/src/java.base/share/classes/sun/security/ssl/X509Authentication.java
index 4e91df2806e..5abc2cb1bf4 100644
--- a/src/java.base/share/classes/sun/security/ssl/X509Authentication.java
+++ b/src/java.base/share/classes/sun/security/ssl/X509Authentication.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2018, 2025, 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
@@ -218,6 +218,28 @@ enum X509Authentication implements SSLAuthentication {
chc.peerSupportedAuthorities == null ? null :
chc.peerSupportedAuthorities.clone(),
engine);
+ } else if (chc.conContext.transport instanceof QuicTLSEngineImpl quicEngineImpl) {
+ // TODO add a method on javax.net.ssl.X509ExtendedKeyManager that
+ // takes QuicTLSEngine.
+ // For now, in context of QUIC, for KeyManager implementations other than
+ // subclasses of sun.security.ssl.X509KeyManagerCertChecking
+ // we don't take into account
+ // any algorithm constraints when choosing the client alias and
+ // just call the functionally limited
+ // javax.net.ssl.X509KeyManager.chooseClientAlias(...)
+ if (km instanceof X509KeyManagerCertChecking xkm) {
+ clientAlias = xkm.chooseQuicClientAlias(keyTypes,
+ chc.peerSupportedAuthorities == null
+ ? null
+ : chc.peerSupportedAuthorities.clone(),
+ quicEngineImpl);
+ } else {
+ clientAlias = km.chooseClientAlias(keyTypes,
+ chc.peerSupportedAuthorities == null
+ ? null
+ : chc.peerSupportedAuthorities.clone(),
+ null);
+ }
}
if (clientAlias == null) {
@@ -290,6 +312,28 @@ enum X509Authentication implements SSLAuthentication {
shc.peerSupportedAuthorities == null ? null :
shc.peerSupportedAuthorities.clone(),
engine);
+ } else if (shc.conContext.transport instanceof QuicTLSEngineImpl quicEngineImpl) {
+ // TODO add a method on javax.net.ssl.X509ExtendedKeyManager that
+ // takes QuicTLSEngine.
+ // For now, in context of QUIC, for KeyManager implementations other than
+ // subclasses of sun.security.ssl.X509KeyManagerCertChecking
+ // we don't take into account
+ // any algorithm constraints when choosing the server alias
+ // and just call the functionally limited
+ // javax.net.ssl.X509KeyManager.chooseServerAlias(...)
+ if (km instanceof X509KeyManagerCertChecking xkm) {
+ serverAlias = xkm.chooseQuicServerAlias(keyType,
+ shc.peerSupportedAuthorities == null
+ ? null
+ : shc.peerSupportedAuthorities.clone(),
+ quicEngineImpl);
+ } else {
+ serverAlias = km.chooseServerAlias(keyType,
+ shc.peerSupportedAuthorities == null
+ ? null
+ : shc.peerSupportedAuthorities.clone(),
+ null);
+ }
}
if (serverAlias == null) {
diff --git a/src/java.base/share/classes/sun/security/ssl/X509KeyManagerCertChecking.java b/src/java.base/share/classes/sun/security/ssl/X509KeyManagerCertChecking.java
index 162a938cddb..9484ab4f830 100644
--- a/src/java.base/share/classes/sun/security/ssl/X509KeyManagerCertChecking.java
+++ b/src/java.base/share/classes/sun/security/ssl/X509KeyManagerCertChecking.java
@@ -74,6 +74,15 @@ abstract class X509KeyManagerCertChecking extends X509ExtendedKeyManager {
abstract boolean isCheckingDisabled();
+ // TODO move this method to a public interface / class
+ abstract String chooseQuicClientAlias(String[] keyTypes, Principal[] issuers,
+ QuicTLSEngineImpl quicTLSEngine);
+
+ // TODO move this method to a public interface / class
+ abstract String chooseQuicServerAlias(String keyType,
+ X500Principal[] issuers,
+ QuicTLSEngineImpl quicTLSEngine);
+
// Entry point to do all certificate checks.
protected EntryStatus checkAlias(int keyStoreIndex, String alias,
Certificate[] chain, Date verificationDate, List keyTypes,
@@ -185,6 +194,17 @@ abstract class X509KeyManagerCertChecking extends X509ExtendedKeyManager {
engine, SIGNATURE_CONSTRAINTS_MODE.PEER, true);
}
+ // Gets algorithm constraints of QUIC TLS engine.
+ protected AlgorithmConstraints getAlgorithmConstraints(QuicTLSEngineImpl engine) {
+
+ if (checksDisabled) {
+ return null;
+ }
+
+ return SSLAlgorithmConstraints.forQUIC(
+ engine, SIGNATURE_CONSTRAINTS_MODE.PEER, true);
+ }
+
// Algorithm constraints check.
private boolean conformsToAlgorithmConstraints(
AlgorithmConstraints constraints, Certificate[] chain,
diff --git a/src/java.base/share/classes/sun/security/ssl/X509KeyManagerImpl.java b/src/java.base/share/classes/sun/security/ssl/X509KeyManagerImpl.java
index df6ecaf7a42..e48096cc363 100644
--- a/src/java.base/share/classes/sun/security/ssl/X509KeyManagerImpl.java
+++ b/src/java.base/share/classes/sun/security/ssl/X509KeyManagerImpl.java
@@ -129,6 +129,13 @@ final class X509KeyManagerImpl extends X509KeyManagerCertChecking {
getAlgorithmConstraints(engine), null, null);
}
+ @Override
+ String chooseQuicClientAlias(String[] keyTypes, Principal[] issuers,
+ QuicTLSEngineImpl quicTLSEngine) {
+ return chooseAlias(getKeyTypes(keyTypes), issuers, CheckType.CLIENT,
+ getAlgorithmConstraints(quicTLSEngine), null, null);
+ }
+
@Override
public String chooseServerAlias(String keyType,
Principal[] issuers, Socket socket) {
@@ -165,6 +172,16 @@ final class X509KeyManagerImpl extends X509KeyManagerCertChecking {
// It is not a really HTTPS endpoint identification.
}
+ @Override
+ String chooseQuicServerAlias(String keyType,
+ X500Principal[] issuers,
+ QuicTLSEngineImpl quicTLSEngine) {
+ return chooseAlias(getKeyTypes(keyType), issuers, CheckType.SERVER,
+ getAlgorithmConstraints(quicTLSEngine),
+ X509TrustManagerImpl.getRequestedServerNames(quicTLSEngine),
+ "HTTPS");
+ }
+
@Override
public String[] getClientAliases(String keyType, Principal[] issuers) {
return getAliases(keyType, issuers, CheckType.CLIENT);
diff --git a/src/java.base/share/classes/sun/security/ssl/X509TrustManagerImpl.java b/src/java.base/share/classes/sun/security/ssl/X509TrustManagerImpl.java
index 5001181fecf..d82b94a1d7d 100644
--- a/src/java.base/share/classes/sun/security/ssl/X509TrustManagerImpl.java
+++ b/src/java.base/share/classes/sun/security/ssl/X509TrustManagerImpl.java
@@ -30,7 +30,9 @@ import java.security.*;
import java.security.cert.*;
import java.util.*;
import java.util.concurrent.locks.ReentrantLock;
+
import javax.net.ssl.*;
+
import sun.security.ssl.SSLAlgorithmConstraints.SIGNATURE_CONSTRAINTS_MODE;
import sun.security.util.AnchorCertificates;
import sun.security.util.HostnameChecker;
@@ -145,6 +147,16 @@ final class X509TrustManagerImpl extends X509ExtendedTrustManager
checkTrusted(chain, authType, engine, false);
}
+ public void checkClientTrusted(X509Certificate[] chain, String authType,
+ QuicTLSEngineImpl quicTLSEngine) throws CertificateException {
+ checkTrusted(chain, authType, quicTLSEngine, true);
+ }
+
+ void checkServerTrusted(X509Certificate[] chain, String authType,
+ QuicTLSEngineImpl quicTLSEngine) throws CertificateException {
+ checkTrusted(chain, authType, quicTLSEngine, false);
+ }
+
private Validator checkTrustedInit(X509Certificate[] chain,
String authType, boolean checkClientTrusted) {
if (chain == null || chain.length == 0) {
@@ -236,6 +248,52 @@ final class X509TrustManagerImpl extends X509ExtendedTrustManager
}
}
+ private void checkTrusted(X509Certificate[] chain,
+ String authType, QuicTLSEngineImpl quicTLSEngine,
+ boolean checkClientTrusted) throws CertificateException {
+ Validator v = checkTrustedInit(chain, authType, checkClientTrusted);
+
+ final X509Certificate[] trustedChain;
+ if (quicTLSEngine != null) {
+
+ final SSLSession session = quicTLSEngine.getHandshakeSession();
+ if (session == null) {
+ throw new CertificateException("No handshake session");
+ }
+
+ // create the algorithm constraints
+ final AlgorithmConstraints constraints = SSLAlgorithmConstraints.forQUIC(
+ quicTLSEngine, SIGNATURE_CONSTRAINTS_MODE.LOCAL, false);
+ final List responseList;
+ // grab any stapled OCSP responses for use in validation
+ if (!checkClientTrusted &&
+ session instanceof ExtendedSSLSession extSession) {
+ responseList = extSession.getStatusResponses();
+ } else {
+ responseList = Collections.emptyList();
+ }
+ // do the certificate chain validation
+ trustedChain = v.validate(chain, null, responseList,
+ constraints, checkClientTrusted ? null : authType);
+
+ // check endpoint identity
+ String identityAlg = quicTLSEngine.getSSLParameters().
+ getEndpointIdentificationAlgorithm();
+ if (identityAlg != null && !identityAlg.isEmpty()) {
+ checkIdentity(session, trustedChain,
+ identityAlg, checkClientTrusted);
+ }
+ } else {
+ trustedChain = v.validate(chain, null, Collections.emptyList(),
+ null, checkClientTrusted ? null : authType);
+ }
+
+ if (SSLLogger.isOn && SSLLogger.isOn("ssl,trustmanager")) {
+ SSLLogger.fine("Found trusted certificate",
+ trustedChain[trustedChain.length - 1]);
+ }
+ }
+
private void checkTrusted(X509Certificate[] chain,
String authType, SSLEngine engine,
boolean checkClientTrusted) throws CertificateException {
@@ -344,6 +402,13 @@ final class X509TrustManagerImpl extends X509ExtendedTrustManager
return Collections.emptyList();
}
+ static List getRequestedServerNames(QuicTLSEngineImpl engine) {
+ if (engine != null) {
+ return getRequestedServerNames(engine.getHandshakeSession());
+ }
+ return Collections.emptyList();
+ }
+
private static List getRequestedServerNames(
SSLSession session) {
if (session instanceof ExtendedSSLSession) {
diff --git a/src/java.base/share/conf/security/java.security b/src/java.base/share/conf/security/java.security
index 32d1ddaf0f7..2464361b9ef 100644
--- a/src/java.base/share/conf/security/java.security
+++ b/src/java.base/share/conf/security/java.security
@@ -971,6 +971,33 @@ jdk.tls.legacyAlgorithms=NULL, anon, RC4, DES, 3DES_EDE_CBC
jdk.tls.keyLimits=AES/GCM/NoPadding KeyUpdate 2^37, \
ChaCha20-Poly1305 KeyUpdate 2^37
+#
+# QUIC TLS key limits on symmetric cryptographic algorithms
+#
+# This security property sets limits on algorithms key usage in QUIC.
+# When the number of encrypted datagrams reaches the algorithm value
+# listed below, key update operation will be initiated.
+#
+# The syntax for the property is described below:
+# KeyLimits:
+# " KeyLimit { , KeyLimit } "
+#
+# KeyLimit:
+# AlgorithmName Length
+#
+# AlgorithmName:
+# A full algorithm transformation.
+#
+# Length:
+# The amount of encrypted data in a session before the Action occurs
+# This value may be an integer value in bytes, or as a power of two, 2^23.
+#
+# Note: This property is currently used by OpenJDK's JSSE implementation. It
+# is not guaranteed to be examined and used by other implementations.
+#
+jdk.quic.tls.keyLimits=AES/GCM/NoPadding 2^23, \
+ ChaCha20-Poly1305 2^23
+
#
# Cryptographic Jurisdiction Policy defaults
#
diff --git a/src/java.net.http/share/classes/java/net/http/HttpClient.java b/src/java.net.http/share/classes/java/net/http/HttpClient.java
index 59afff013c7..4ce77486e70 100644
--- a/src/java.net.http/share/classes/java/net/http/HttpClient.java
+++ b/src/java.net.http/share/classes/java/net/http/HttpClient.java
@@ -28,9 +28,11 @@ package java.net.http;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.InetAddress;
+import java.net.http.HttpOption.Http3DiscoveryMode;
import java.net.http.HttpResponse.BodyHandlers;
import java.net.http.HttpResponse.BodySubscriber;
import java.net.http.HttpResponse.BodySubscribers;
+import java.net.URI;
import java.nio.channels.Selector;
import java.net.Authenticator;
import java.net.CookieHandler;
@@ -59,7 +61,7 @@ import jdk.internal.net.http.HttpClientBuilderImpl;
* The {@link #newBuilder() newBuilder} method returns a builder that creates
* instances of the default {@code HttpClient} implementation.
* The builder can be used to configure per-client state, like: the preferred
- * protocol version ( HTTP/1.1 or HTTP/2 ), whether to follow redirects, a
+ * protocol version ( HTTP/1.1, HTTP/2 or HTTP/3 ), whether to follow redirects, a
* proxy, an authenticator, etc. Once built, an {@code HttpClient} is immutable,
* and can be used to send multiple requests.
*
@@ -162,6 +164,59 @@ import jdk.internal.net.http.HttpClientBuilderImpl;
* prevent the resources allocated by the associated client from
* being reclaimed by the garbage collector.
*
+ *
+ * The default implementation of the {@code HttpClient} supports HTTP/1.1,
+ * HTTP/2, and HTTP/3. Which version of the protocol is actually used when sending
+ * a request can depend on multiple factors. In the case of HTTP/2, it may depend
+ * on an initial upgrade to succeed (when using a plain connection), or on HTTP/2
+ * being successfully negotiated during the Transport Layer Security (TLS) handshake.
+ *
+ *
If {@linkplain Version#HTTP_2 HTTP/2} is selected over a clear
+ * connection, and no HTTP/2 connection to the
+ * origin server
+ * already exists, the client will create a new connection and attempt an upgrade
+ * from HTTP/1.1 to HTTP/2.
+ * If the upgrade succeeds, then the response to this request will use HTTP/2.
+ * If the upgrade fails, then the response will be handled using HTTP/1.1.
+ *
+ *
Other constraints may also affect the selection of protocol version.
+ * For example, if HTTP/2 is requested through a proxy, and if the implementation
+ * does not support this mode, then HTTP/1.1 may be used.
+ *
+ * The HTTP/3 protocol is not selected by default, but can be enabled by setting
+ * the {@linkplain Builder#version(Version) HttpClient preferred version} or the
+ * {@linkplain HttpRequest.Builder#version(Version) HttpRequest preferred version} to
+ * {@linkplain Version#HTTP_3 HTTP/3}. Like for HTTP/2, which protocol version is
+ * actually used when HTTP/3 is enabled may depend on several factors.
+ * {@linkplain HttpOption#H3_DISCOVERY Configuration hints} can
+ * be {@linkplain HttpRequest.Builder#setOption(HttpOption, Object) provided}
+ * to help the {@code HttpClient} implementation decide how to establish
+ * and carry out the HTTP exchange when the HTTP/3 protocol is enabled.
+ * If no configuration hints are provided, the {@code HttpClient} will select
+ * one as explained in the {@link HttpOption#H3_DISCOVERY H3_DISCOVERY}
+ * option API documentation.
+ *
Note that a request whose {@linkplain URI#getScheme() URI scheme} is not
+ * {@code "https"} will never be sent over HTTP/3. In this implementation,
+ * HTTP/3 is not used if a proxy is selected.
+ *
+ *
+ * If a concrete instance of {@link HttpClient} doesn't support sending a
+ * request through HTTP/3, an {@link UnsupportedProtocolVersionException} may be
+ * thrown, either when {@linkplain Builder#build() building} the client with
+ * a {@linkplain Builder#version(Version) preferred version} set to HTTP/3,
+ * or when attempting to send a request with {@linkplain HttpRequest.Builder#version(Version)
+ * HTTP/3 enabled} when {@link Http3DiscoveryMode#HTTP_3_URI_ONLY HTTP_3_URI_ONLY}
+ * was {@linkplain HttpRequest.Builder#setOption(HttpOption, Object) specified}.
+ * This may typically happen if the {@link #sslContext() SSLContext}
+ * or {@link #sslParameters() SSLParameters} configured on the client instance cannot
+ * be used with HTTP/3.
+ *
+ * @see UnsupportedProtocolVersionException
+ * @see Builder#version(Version)
+ * @see HttpRequest.Builder#version(Version)
+ * @see HttpRequest.Builder#setOption(HttpOption, Object)
+ * @see HttpOption#H3_DISCOVERY
+ *
* @since 11
*/
public abstract class HttpClient implements AutoCloseable {
@@ -320,23 +375,19 @@ public abstract class HttpClient implements AutoCloseable {
public Builder followRedirects(Redirect policy);
/**
- * Requests a specific HTTP protocol version where possible.
+ * Sets the default preferred HTTP protocol version for requests
+ * issued by this client.
*
*
If this method is not invoked prior to {@linkplain #build()
* building}, then newly built clients will prefer {@linkplain
* Version#HTTP_2 HTTP/2}.
*
- *
If set to {@linkplain Version#HTTP_2 HTTP/2}, then each request
- * will attempt to upgrade to HTTP/2. If the upgrade succeeds, then the
- * response to this request will use HTTP/2 and all subsequent requests
- * and responses to the same
- * origin server
- * will use HTTP/2. If the upgrade fails, then the response will be
- * handled using HTTP/1.1
+ *
If a request doesn't have a preferred version, then
+ * the effective preferred version for the request is the
+ * client's preferred version.
*
- * @implNote Constraints may also affect the selection of protocol version.
- * For example, if HTTP/2 is requested through a proxy, and if the implementation
- * does not support this mode, then HTTP/1.1 may be used
+ * @implNote Some constraints may also affect the {@linkplain
+ * HttpClient##ProtocolVersionSelection selection of the actual protocol version}.
*
* @param version the requested HTTP protocol version
* @return this builder
@@ -439,9 +490,14 @@ public abstract class HttpClient implements AutoCloseable {
* @return a new {@code HttpClient}
*
* @throws UncheckedIOException may be thrown if underlying IO resources required
- * by the implementation cannot be allocated. For instance,
+ * by the implementation cannot be allocated, or if the resulting configuration
+ * does not satisfy the implementation requirements. For instance,
* if the implementation requires a {@link Selector}, and opening
- * one fails due to {@linkplain Selector#open() lack of necessary resources}.
+ * one fails due to {@linkplain Selector#open() lack of necessary resources},
+ * or if the {@linkplain #version(Version) preferred protocol version} is not
+ * {@linkplain HttpClient##UnsupportedProtocolVersion supported by
+ * the implementation or cannot be used in this configuration}.
+ *
*/
public HttpClient build();
}
@@ -525,9 +581,11 @@ public abstract class HttpClient implements AutoCloseable {
* Returns the preferred HTTP protocol version for this client. The default
* value is {@link HttpClient.Version#HTTP_2}
*
- * @implNote Constraints may also affect the selection of protocol version.
- * For example, if HTTP/2 is requested through a proxy, and if the
- * implementation does not support this mode, then HTTP/1.1 may be used
+ * @implNote
+ * The protocol version that the {@code HttpClient} eventually
+ * decides to use for a request is affected by various factors noted
+ * in {@linkplain ##ProtocolVersionSelection protocol version selection}
+ * section.
*
* @return the HTTP protocol version requested
*/
@@ -562,7 +620,13 @@ public abstract class HttpClient implements AutoCloseable {
/**
* HTTP version 2
*/
- HTTP_2
+ HTTP_2,
+
+ /**
+ * HTTP version 3
+ * @since 26
+ */
+ HTTP_3
}
/**
diff --git a/src/java.net.http/share/classes/java/net/http/HttpOption.java b/src/java.net.http/share/classes/java/net/http/HttpOption.java
new file mode 100644
index 00000000000..cbff11f71ee
--- /dev/null
+++ b/src/java.net.http/share/classes/java/net/http/HttpOption.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (c) 2025, 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. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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 java.net.http;
+
+import java.net.ProxySelector;
+import java.net.URI;
+import java.net.http.HttpClient.Version;
+import java.net.http.HttpRequest.Builder;
+
+/**
+ * This interface is used to provide additional request configuration
+ * option hints on how an HTTP request/response exchange should
+ * be carried out by the {@link HttpClient} implementation.
+ * Request configuration option hints can be provided to an
+ * {@link HttpRequest} with the {@link
+ * Builder#setOption(HttpOption, Object) HttpRequest.Builder
+ * setOption} method.
+ *
+ * Concrete instances of this class and its subclasses are immutable.
+ *
+ * @apiNote
+ * In this version, the {@code HttpOption} interface is sealed and
+ * only allows the {@link #H3_DISCOVERY} option. However, it could be
+ * extended in the future to support additional options.
+ *
+ * The {@link #H3_DISCOVERY} option can be used to help the
+ * {@link HttpClient} decide how to select or establish an
+ * HTTP/3 connection through which to carry out an HTTP/3
+ * request/response exchange.
+ *
+ * @param The {@linkplain #type() type of the option value}
+ *
+ * @since 26
+ */
+public sealed interface HttpOption permits HttpRequestOptionImpl {
+ /**
+ * {@return the option name}
+ *
+ * @implSpec Different options must have different names.
+ */
+ String name();
+
+ /**
+ * {@return the type of the value associated with the option}
+ *
+ * @apiNote Different options may have the same type.
+ */
+ Class type();
+
+ /**
+ * An option that can be used to configure how the {@link HttpClient} will
+ * select or establish an HTTP/3 connection through which to carry out
+ * the request. If {@link Version#HTTP_3} is not selected either as
+ * the {@linkplain Builder#version(Version) request preferred version}
+ * or the {@linkplain HttpClient.Builder#version(Version) HttpClient
+ * preferred version} setting this option on the request has no effect.
+ *
+ * The {@linkplain #name() name of this option} is {@code "H3_DISCOVERY"}.
+ *
+ * @implNote
+ * The JDK built-in implementation of the {@link HttpClient} understands the
+ * request option {@link #H3_DISCOVERY} hint.
+ *
+ * If no {@code H3_DISCOVERY} hint is provided, and the {@linkplain Version#HTTP_3
+ * HTTP/3 version} is selected, either as {@linkplain Builder#version(Version)
+ * request preferred version} or {@linkplain HttpClient.Builder#version(Version)
+ * client preferred version}, the JDK built-in implementation will establish
+ * the exchange as per {@link Http3DiscoveryMode#ANY}.
+ *
+ * In case of {@linkplain HttpClient.Redirect redirect}, the
+ * {@link #H3_DISCOVERY} option, if present, is always transferred to
+ * the new request.
+ *
+ * In this implementation, HTTP/3 through proxies is not supported.
+ * Unless {@link Http3DiscoveryMode#HTTP_3_URI_ONLY} is specified, if
+ * a {@linkplain HttpClient.Builder#proxy(ProxySelector) proxy} is {@linkplain
+ * ProxySelector#select(URI) selected} for the {@linkplain HttpRequest#uri()
+ * request URI}, the protocol version is downgraded to HTTP/2 or
+ * HTTP/1.1 and the {@link #H3_DISCOVERY} option is ignored. If, on the
+ * other hand, {@link Http3DiscoveryMode#HTTP_3_URI_ONLY} is specified,
+ * the request will fail.
+ *
+ * @see Http3DiscoveryMode
+ * @see Builder#setOption(HttpOption, Object)
+ */
+ HttpOption H3_DISCOVERY =
+ new HttpRequestOptionImpl<>(Http3DiscoveryMode.class, "H3_DISCOVERY");
+
+ /**
+ * This enumeration can be used to help the {@link HttpClient} decide
+ * how an HTTP/3 exchange should be established, and can be provided
+ * as the value of the {@link HttpOption#H3_DISCOVERY} option
+ * to {@link Builder#setOption(HttpOption, Object) Builder.setOption}.
+ *
+ * Note that if neither the {@linkplain Builder#version(Version) request preferred
+ * version} nor the {@linkplain HttpClient.Builder#version(Version) client preferred
+ * version} is {@linkplain Version#HTTP_3 HTTP/3}, no HTTP/3 exchange will
+ * be established and the {@code Http3DiscoveryMode} is ignored.
+ *
+ * @since 26
+ */
+ enum Http3DiscoveryMode {
+ /**
+ * This instructs the {@link HttpClient} to use its own implementation
+ * specific algorithm to find or establish a connection for the exchange.
+ * Typically, if no connection was previously established with the origin
+ * server defined by the request URI, the {@link HttpClient} implementation
+ * may attempt to establish both an HTTP/3 connection over QUIC and an HTTP
+ * connection over TLS/TCP at the authority present in the request URI,
+ * and use the first that succeeds. The exchange may then be carried out with
+ * any of the {@linkplain Version
+ * three HTTP protocol versions}, depending on which method succeeded first.
+ *
+ * @implNote
+ * If the {@linkplain Builder#version(Version) request preferred version} is {@linkplain
+ * Version#HTTP_3 HTTP/3}, the {@code HttpClient} may give priority to HTTP/3 by
+ * attempting to establish an HTTP/3 connection, before attempting a TLS
+ * connection over TCP.
+ *
+ * When attempting an HTTP/3 connection in this mode, the {@code HttpClient} may
+ * use any HTTP Alternative Services
+ * information it may have previously obtained from the origin server. If no
+ * such information is available, a direct HTTP/3 connection at the authority (host, port)
+ * present in the {@linkplain HttpRequest#uri() request URI} will be attempted.
+ */
+ ANY,
+ /**
+ * This instructs the {@link HttpClient} to only use the
+ * HTTP Alternative Services
+ * to find or establish an HTTP/3 connection with the origin server.
+ * The exchange may then be carried out with any of the {@linkplain
+ * Version three HTTP protocol versions}, depending on
+ * whether an Alternate Service record for HTTP/3 could be found, and which HTTP version
+ * was negotiated with the origin server, if no such record could be found.
+ *
+ * In this mode, requests sent to the origin server will be sent through HTTP/1.1 or HTTP/2
+ * until a {@code h3} HTTP Alternative Services
+ * endpoint for that server is advertised to the client. Usually, an alternate service is
+ * advertised by a server when responding to a request, so that subsequent requests can make
+ * use of that alternative service.
+ */
+ ALT_SVC,
+ /**
+ * This instructs the {@link HttpClient} to only attempt an HTTP/3 connection
+ * with the origin server. The connection will only succeed if the origin server
+ * is listening for incoming HTTP/3 connections over QUIC at the same authority (host, port)
+ * as defined in the {@linkplain HttpRequest#uri() request URI}. In this mode,
+ * HTTP Alternative Services
+ * are not used.
+ */
+ HTTP_3_URI_ONLY
+ }
+
+}
diff --git a/src/java.net.http/share/classes/java/net/http/HttpRequest.java b/src/java.net.http/share/classes/java/net/http/HttpRequest.java
index 84a521336b6..c56328ba4b4 100644
--- a/src/java.net.http/share/classes/java/net/http/HttpRequest.java
+++ b/src/java.net.http/share/classes/java/net/http/HttpRequest.java
@@ -29,6 +29,7 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
+import java.net.http.HttpClient.Version;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
@@ -91,6 +92,24 @@ public abstract class HttpRequest {
*/
protected HttpRequest() {}
+ /**
+ * {@return the value configured on this request for the given option, if any}
+ * @param option a request configuration option
+ * @param the type of the option
+ *
+ * @see Builder#setOption(HttpOption, Object)
+ *
+ * @implSpec
+ * The default implementation of this method returns {@link Optional#empty()}
+ * if {@code option} is non-null, otherwise throws {@link NullPointerException}.
+ *
+ * @since 26
+ */
+ public Optional getOption(HttpOption option) {
+ Objects.requireNonNull(option);
+ return Optional.empty();
+ }
+
/**
* A builder of {@linkplain HttpRequest HTTP requests}.
*
@@ -144,14 +163,53 @@ public abstract class HttpRequest {
*
* The corresponding {@link HttpResponse} should be checked for the
* version that was actually used. If the version is not set in a
- * request, then the version requested will be that of the sending
- * {@link HttpClient}.
+ * request, then the version requested will be {@linkplain
+ * HttpClient.Builder#version(Version) that of the sending
+ * {@code HttpClient}}.
+ *
+ * @implNote
+ * Constraints may also affect the {@linkplain HttpClient##ProtocolVersionSelection
+ * selection of the actual protocol version}.
*
* @param version the HTTP protocol version requested
* @return this builder
*/
public Builder version(HttpClient.Version version);
+ /**
+ * Provides request configuration option hints modeled as key value pairs
+ * to help an {@link HttpClient} implementation decide how the
+ * request/response exchange should be established or carried out.
+ *
+ *
An {@link HttpClient} implementation may decide to ignore request
+ * configuration option hints, or fail the request, if provided with any
+ * option hints that it does not understand.
+ *
+ * If this method is invoked twice for the same {@linkplain HttpOption
+ * request option}, any value previously provided to this builder for the
+ * corresponding option is replaced by the new value.
+ * If {@code null} is supplied as a value, any value previously
+ * provided is discarded.
+ *
+ * @implSpec
+ * The default implementation of this method discards the provided option
+ * hint and does nothing.
+ *
+ * @implNote
+ * The JDK built-in implementation of the {@link HttpClient} understands the
+ * request option {@link HttpOption#H3_DISCOVERY} hint.
+ *
+ * @param option the request configuration option
+ * @param value the request configuration option value (can be null)
+ *
+ * @return this builder
+ *
+ * @see HttpRequest#getOption(HttpOption)
+ *
+ * @since 26
+ */
+ public default Builder setOption(HttpOption option, T value) { return this; }
+
/**
* Adds the given name value pair to the set of headers for this request.
* The given value is added to the list of values for that name.
@@ -394,6 +452,8 @@ public abstract class HttpRequest {
}
}
);
+ request.getOption(HttpOption.H3_DISCOVERY)
+ .ifPresent(opt -> builder.setOption(HttpOption.H3_DISCOVERY, opt));
return builder;
}
diff --git a/src/java.net.http/share/classes/java/net/http/HttpRequestOptionImpl.java b/src/java.net.http/share/classes/java/net/http/HttpRequestOptionImpl.java
new file mode 100644
index 00000000000..f5562c7068b
--- /dev/null
+++ b/src/java.net.http/share/classes/java/net/http/HttpRequestOptionImpl.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2025, 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. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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 java.net.http;
+
+// Package private implementation of HttpRequest options
+record HttpRequestOptionImpl(Class type, String name)
+ implements HttpOption {
+ @Override
+ public String toString() {
+ return name();
+ }
+}
diff --git a/src/java.net.http/share/classes/java/net/http/HttpResponse.java b/src/java.net.http/share/classes/java/net/http/HttpResponse.java
index 52f5298452a..9843e4c7c5b 100644
--- a/src/java.net.http/share/classes/java/net/http/HttpResponse.java
+++ b/src/java.net.http/share/classes/java/net/http/HttpResponse.java
@@ -803,24 +803,66 @@ public interface HttpResponse {
/**
* A handler for push promises.
*
- * A push promise is a synthetic request sent by an HTTP/2 server
+ *
A push promise is a synthetic request sent by an HTTP/2 or HTTP/3 server
* when retrieving an initiating client-sent request. The server has
* determined, possibly through inspection of the initiating request, that
* the client will likely need the promised resource, and hence pushes a
* synthetic push request, in the form of a push promise, to the client. The
* client can choose to accept or reject the push promise request.
*
- *
A push promise request may be received up to the point where the
+ *
For HTTP/2, a push promise request may be received up to the point where the
* response body of the initiating client-sent request has been fully
* received. The delivery of a push promise response, however, is not
* coordinated with the delivery of the response to the initiating
- * client-sent request.
+ * client-sent request. These are delivered with the
+ * {@link #applyPushPromise(HttpRequest, HttpRequest, Function)} method.
+ *
+ * For HTTP/3, push promises are handled in a similar way, except that promises
+ * of the same resource (request URI, request headers and response body) can be
+ * promised multiple times, but are only delivered by the server (and this API)
+ * once though the method {@link #applyPushPromise(HttpRequest, HttpRequest, PushId, Function)}.
+ * Subsequent promises of the same resource, receive a notification only
+ * of the promise by the method {@link #notifyAdditionalPromise(HttpRequest, PushId)}.
+ * The same {@link PushPromiseHandler.PushId} is supplied for each of these
+ * notifications. Additionally, HTTP/3 push promises are not restricted to a context
+ * of a single initiating request. The same push promise can be delivered and then notified
+ * across multiple client initiated requests within the same HTTP/3 (QUIC) connection.
*
* @param the push promise response body type
* @since 11
*/
public interface PushPromiseHandler {
+ /**
+ * Represents a HTTP/3 PushID. PushIds can be shared across
+ * multiple client initiated requests on the same HTTP/3 connection.
+ * @since 26
+ */
+ public sealed interface PushId {
+
+ /**
+ * Represents an HTTP/3 PushId.
+ *
+ * @param pushId the pushId as a long
+ * @param connectionLabel the {@link HttpResponse#connectionLabel()}
+ * of the HTTP/3 connection
+ * @apiNote
+ * The {@code connectionLabel} should be considered opaque, and ensures that
+ * two long pushId emitted by different connections correspond to distinct
+ * instances of {@code PushId}. The {@code pushId} corresponds to the
+ * unique push ID assigned by the server that identifies a given server
+ * push on that connection, as defined by
+ * RFC 9114,
+ * section 4.6
+ *
+ * @spec https://www.rfc-editor.org/info/rfc9114
+ * RFC 9114: HTTP/3
+ *
+ * @since 26
+ */
+ record Http3PushId(long pushId, String connectionLabel) implements PushId { }
+ }
+
/**
* Notification of an incoming push promise.
*
@@ -838,6 +880,12 @@ public interface HttpResponse {
* then the push promise is rejected. The {@code acceptor} function will
* throw an {@code IllegalStateException} if invoked more than once.
*
+ * This method is invoked for all HTTP/2 push promises and also
+ * by default for the first promise of all HTTP/3 push promises.
+ * If {@link #applyPushPromise(HttpRequest, HttpRequest, PushId, Function)}
+ * is overridden, then this method is not directly invoked for HTTP/3
+ * push promises.
+ *
* @param initiatingRequest the initiating client-send request
* @param pushPromiseRequest the synthetic push request
* @param acceptor the acceptor function that must be successfully
@@ -849,6 +897,67 @@ public interface HttpResponse {
Function,CompletableFuture>> acceptor
);
+ /**
+ * Notification of the first occurrence of an HTTP/3 incoming push promise.
+ *
+ * Subsequent promises of the same resource (with the same PushId) are notified
+ * using {@link #notifyAdditionalPromise(HttpRequest, PushId)
+ * notifyAdditionalPromise(HttpRequest, PushId)}.
+ *
+ * This method is invoked once for each push promise received, up
+ * to the point where the response body of the initiating client-sent
+ * request has been fully received.
+ *
+ *
A push promise is accepted by invoking the given {@code acceptor}
+ * function. The {@code acceptor} function must be passed a non-null
+ * {@code BodyHandler}, that is to be used to handle the promise's
+ * response body. The acceptor function will return a {@code
+ * CompletableFuture} that completes with the promise's response.
+ *
+ *
If the {@code acceptor} function is not successfully invoked,
+ * then the push promise is rejected. The {@code acceptor} function will
+ * throw an {@code IllegalStateException} if invoked more than once.
+ *
+ * @implSpec the default implementation invokes
+ * {@link #applyPushPromise(HttpRequest, HttpRequest, Function)}. This allows
+ * {@code PushPromiseHandlers} from previous releases to handle HTTP/3 push
+ * promise in a reasonable way.
+ *
+ * @param initiatingRequest the client request that resulted in the promise
+ * @param pushPromiseRequest the promised HttpRequest from the server
+ * @param pushid the PushId which can be linked to subsequent notifications
+ * @param acceptor the acceptor function that must be successfully
+ * invoked to accept the push promise
+ *
+ * @since 26
+ */
+ public default void applyPushPromise(
+ HttpRequest initiatingRequest,
+ HttpRequest pushPromiseRequest,
+ PushId pushid,
+ Function,CompletableFuture>> acceptor
+ ) {
+ applyPushPromise(initiatingRequest, pushPromiseRequest, acceptor);
+ }
+
+ /**
+ * Invoked for each additional HTTP/3 Push Promise. The {@code pushid} links the promise to the
+ * original promised {@link HttpRequest} and {@link HttpResponse}. Additional promises
+ * generally result from different client initiated requests.
+ *
+ * @implSpec
+ * The default implementation of this method does nothing.
+ *
+ * @param initiatingRequest the client initiated request which resulted in the push
+ * @param pushid the pushid which may have been notified previously
+ *
+ * @since 26
+ */
+ public default void notifyAdditionalPromise(
+ HttpRequest initiatingRequest,
+ PushId pushid
+ ) {
+ }
/**
* Returns a push promise handler that accumulates push promises, and
@@ -915,7 +1024,7 @@ public interface HttpResponse {
*
* @apiNote To ensure that all resources associated with the corresponding
* HTTP exchange are properly released, an implementation of {@code
- * BodySubscriber} should ensure to {@linkplain Flow.Subscription#request
+ * BodySubscriber} should ensure to {@linkplain Flow.Subscription#request(long)
* request} more data until one of {@link #onComplete() onComplete} or
* {@link #onError(Throwable) onError} are signalled, or {@link
* Flow.Subscription#cancel cancel} its {@linkplain
@@ -957,7 +1066,7 @@ public interface HttpResponse {
* {@snippet :
* // Streams the response body to a File
* HttpResponse response = client
- * .send(request, responseInfo -> BodySubscribers.ofFile(Paths.get("example.html")); }
+ * .send(request, responseInfo -> BodySubscribers.ofFile(Paths.get("example.html"))); }
*
* {@snippet :
* // Accumulates the response body and returns it as a byte[]
diff --git a/src/java.net.http/share/classes/java/net/http/StreamLimitException.java b/src/java.net.http/share/classes/java/net/http/StreamLimitException.java
new file mode 100644
index 00000000000..583b515b01b
--- /dev/null
+++ b/src/java.net.http/share/classes/java/net/http/StreamLimitException.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (c) 2023, 2025, 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. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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 java.net.http;
+
+import java.io.IOException;
+import java.io.InvalidObjectException;
+import java.io.ObjectInputStream;
+import java.net.http.HttpClient.Version;
+import java.net.http.HttpResponse.BodyHandler;
+import java.net.http.HttpResponse.PushPromiseHandler;
+import java.util.Objects;
+
+/**
+ * An exception raised when the limit imposed for stream creation on an
+ * HTTP connection is reached, and the client is unable to create a new
+ * stream.
+ *
+ * A {@code StreamLimitException} may be raised when attempting to send
+ * a new request on any {@linkplain #version()
+ * protocol version} that supports multiplexing on a single connection. Both
+ * {@linkplain HttpClient.Version#HTTP_2 HTTP/2} and {@linkplain
+ * HttpClient.Version#HTTP_3 HTTP/3} allow multiplexing concurrent requests
+ * to the same server on a single connection. Each request/response exchange
+ * is carried over a single stream, as defined by the corresponding
+ * protocol.
+ *
+ * Whether and when a {@code StreamLimitException} may be
+ * relayed to the code initiating a request/response exchange is
+ * implementation and protocol version dependent.
+ *
+ * @see HttpClient#send(HttpRequest, BodyHandler)
+ * @see HttpClient#sendAsync(HttpRequest, BodyHandler)
+ * @see HttpClient#sendAsync(HttpRequest, BodyHandler, PushPromiseHandler)
+ *
+ * @since 26
+ */
+public final class StreamLimitException extends IOException {
+
+ @java.io.Serial
+ private static final long serialVersionUID = 2614981180406031159L;
+
+ /**
+ * The version of the HTTP protocol on which the stream limit exception occurred.
+ * Must not be null.
+ * @serial
+ */
+ private final Version version;
+
+ /**
+ * Creates a new {@code StreamLimitException}
+ * @param version the version of the protocol on which the stream limit exception
+ * occurred. Must not be null.
+ * @param message the detailed exception message, which can be null.
+ */
+ public StreamLimitException(final Version version, final String message) {
+ super(message);
+ this.version = Objects.requireNonNull(version);
+ }
+
+ /**
+ * {@return the protocol version for which the exception was raised}
+ */
+ public final Version version() {
+ return version;
+ }
+
+ /**
+ * Restores the state of a {@code StreamLimitException} from the stream
+ * @param in the input stream
+ * @throws IOException if the class of a serialized object could not be found.
+ * @throws ClassNotFoundException if an I/O error occurs.
+ * @throws InvalidObjectException if {@code version} is null.
+ */
+ @java.io.Serial
+ private void readObject(ObjectInputStream in)
+ throws IOException, ClassNotFoundException {
+ in.defaultReadObject();
+ if (version == null) {
+ throw new InvalidObjectException("version must not be null");
+ }
+ }
+}
diff --git a/src/java.net.http/share/classes/java/net/http/UnsupportedProtocolVersionException.java b/src/java.net.http/share/classes/java/net/http/UnsupportedProtocolVersionException.java
new file mode 100644
index 00000000000..eecc039e5d2
--- /dev/null
+++ b/src/java.net.http/share/classes/java/net/http/UnsupportedProtocolVersionException.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2022, 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. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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 java.net.http;
+
+import java.io.IOException;
+import java.io.Serial;
+import java.net.http.HttpClient.Builder;
+
+/**
+ * Thrown when the HTTP client doesn't support a particular HTTP version.
+ * @apiNote
+ * Typically, this exception may be thrown when attempting to
+ * {@linkplain Builder#build() build} an {@link java.net.http.HttpClient}
+ * configured to use {@linkplain java.net.http.HttpClient.Version#HTTP_3
+ * HTTP version 3} by default, when the underlying {@link javax.net.ssl.SSLContext
+ * SSLContext} implementation does not meet the requirements for supporting
+ * the HttpClient's implementation of the underlying QUIC transport protocol.
+ * @since 26
+ */
+public final class UnsupportedProtocolVersionException extends IOException {
+
+ @Serial
+ private static final long serialVersionUID = 981344214212332893L;
+
+ /**
+ * Constructs an {@code UnsupportedProtocolVersionException} with the given detail message.
+ *
+ * @param message The detail message; can be {@code null}
+ */
+ public UnsupportedProtocolVersionException(String message) {
+ super(message);
+ }
+}
diff --git a/src/java.net.http/share/classes/java/net/http/package-info.java b/src/java.net.http/share/classes/java/net/http/package-info.java
index 9958fd94da0..1b8395c2706 100644
--- a/src/java.net.http/share/classes/java/net/http/package-info.java
+++ b/src/java.net.http/share/classes/java/net/http/package-info.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2025, 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
@@ -26,7 +26,7 @@
/**
*
HTTP Client and WebSocket APIs
*
- * Provides high-level client interfaces to HTTP (versions 1.1 and 2) and
+ *
Provides high-level client interfaces to HTTP (versions 1.1, 2, and 3) and
* low-level client interfaces to WebSocket. The main types defined are:
*
*
*
* The protocol-specific requirements are defined in the
- * Hypertext Transfer Protocol
- * Version 2 (HTTP/2), the
+ * Hypertext Transfer Protocol
+ * Version 3 (HTTP/3), the
+ * Hypertext Transfer Protocol Version 2 (HTTP/2), the
+ *
* Hypertext Transfer Protocol (HTTP/1.1), and
- * The WebSocket Protocol.
+ * The WebSocket Protocol.
*
*
In general, asynchronous tasks execute in either the thread invoking
* the operation, e.g. {@linkplain HttpClient#send(HttpRequest, BodyHandler)
@@ -66,6 +68,15 @@
*
Unless otherwise stated, {@code null} parameter values will cause methods
* of all classes in this package to throw {@code NullPointerException}.
*
+ * @spec https://www.rfc-editor.org/info/rfc9114
+ * RFC 9114: HTTP/3
+ * @spec https://www.rfc-editor.org/info/rfc7540
+ * RFC 7540: Hypertext Transfer Protocol Version 2 (HTTP/2)
+ * @spec https://www.rfc-editor.org/info/rfc2616
+ * RFC 2616: Hypertext Transfer Protocol -- HTTP/1.1
+ * @spec https://www.rfc-editor.org/info/rfc6455
+ * RFC 6455: The WebSocket Protocol
+ *
* @since 11
*/
package java.net.http;
diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/AltServicesRegistry.java b/src/java.net.http/share/classes/jdk/internal/net/http/AltServicesRegistry.java
new file mode 100644
index 00000000000..08161bcd110
--- /dev/null
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/AltServicesRegistry.java
@@ -0,0 +1,569 @@
+/*
+ * Copyright (c) 2020, 2025, 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. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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.internal.net.http;
+
+import java.net.URI;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.function.Predicate;
+import java.util.stream.Stream;
+
+import javax.net.ssl.SNIServerName;
+
+import jdk.internal.net.http.common.Deadline;
+import jdk.internal.net.http.common.Log;
+import jdk.internal.net.http.common.Logger;
+import jdk.internal.net.http.common.TimeSource;
+import jdk.internal.net.http.common.Utils;
+
+/**
+ * A registry for Alternate Services advertised by server endpoints.
+ * There is one registry per HttpClient.
+ */
+public final class AltServicesRegistry {
+
+ // id and logger for debugging purposes: the id is the same for the HttpClientImpl.
+ private final long id;
+ private final Logger debug = Utils.getDebugLogger(this::dbgString);
+
+ // The key is the origin of the alternate service
+ // The value is a list of AltService records declared by the origin.
+ private final Map> altServices = new HashMap<>();
+ // alt services which were marked invalid in context of an origin. the reason for
+ // them being invalid can be connection issues (for example: the alt service didn't present the
+ // certificate of the origin)
+ private final InvalidAltServices invalidAltServices = new InvalidAltServices();
+
+ // used while dealing with both altServices Map and the invalidAltServices Set
+ private final ReentrantLock registryLock = new ReentrantLock();
+
+ public AltServicesRegistry(long id) {
+ this.id = id;
+ }
+
+ String dbgString() {
+ return "AltServicesRegistry(" + id + ")";
+ }
+
+ public static final class AltService {
+ // As defined in RFC-7838, section 2, formally an alternate service is a combination of
+ // ALPN, host and port
+ public record Identity(String alpn, String host, int port) {
+ public Identity {
+ Objects.requireNonNull(alpn);
+ Objects.requireNonNull(host);
+ if (port <= 0) {
+ throw new IllegalArgumentException("Invalid port: " + port);
+ }
+ }
+
+ public boolean matches(AltService service) {
+ return equals(service.identity());
+ }
+
+ @Override
+ public String toString() {
+ return alpn + "=\"" + Origin.toAuthority(host, port) +"\"";
+ }
+ }
+
+ private record AltServiceData(Identity id, Origin origin, Deadline deadline,
+ boolean persist, boolean advertised,
+ String authority,
+ boolean sameAuthorityAsOrigin) {
+ public String pretty() {
+ return "AltSvc: " + id +
+ "; origin=\"" + origin + "\"" +
+ "; deadline=" + deadline +
+ "; persist=" + persist +
+ "; advertised=" + advertised +
+ "; sameAuthorityAsOrigin=" + sameAuthorityAsOrigin +
+ ';';
+ }
+ }
+ private final AltServiceData svc;
+
+ /**
+ * @param id the alpn, host and port of this alternate service
+ * @param origin the {@link Origin} for this alternate service
+ * @param deadline the deadline until which this endpoint is valid
+ * @param persist whether that information can be persisted (we don't use this)
+ * @param advertised Whether or not this alt service was advertised as an alt service.
+ * In certain cases, an alt service is created when no origin server
+ * has advertised it. In those cases, this param is {@code false}
+ */
+ private AltService(final Identity id, final Origin origin, Deadline deadline,
+ final boolean persist,
+ final boolean advertised) {
+ Objects.requireNonNull(id);
+ Objects.requireNonNull(origin);
+ assert origin.isSecure() : "origin " + origin + " is not secure";
+ deadline = deadline == null ? Deadline.MAX : deadline;
+ final String authority = Origin.toAuthority(id.host, id.port);
+ final String originAuthority = Origin.toAuthority(origin.host(), origin.port());
+ // keep track of whether the authority of this alt service is same as that
+ // of the origin
+ final boolean sameAuthorityAsOrigin = authority.equals(originAuthority);
+ svc = new AltServiceData(id, origin, deadline, persist, advertised,
+ authority, sameAuthorityAsOrigin);
+ }
+
+ public Identity identity() {
+ return svc.id;
+ }
+
+ /**
+ * @return {@code host:port} of the alternate service
+ */
+ public String authority() {
+ return svc.authority;
+ }
+
+ /**
+ * @return {@code identity().host()}
+ */
+ public String host() {
+ return svc.id.host;
+ }
+
+ /**
+ * @return {@code identity().port()}
+ */
+ public int port() {
+ return svc.id.port;
+ }
+
+ public boolean isPersist() {
+ return svc.persist;
+ }
+
+ public boolean wasAdvertised() {
+ return svc.advertised;
+ }
+
+ public String alpn() {
+ return svc.id.alpn;
+ }
+
+ public Origin origin() {
+ return svc.origin;
+ }
+
+ public Deadline deadline() {
+ return svc.deadline;
+ }
+
+ /**
+ * {@return true if the origin, for which this is an alternate service, has the
+ * same authority as this alternate service. false otherwise.}
+ */
+ public boolean originHasSameAuthority() {
+ return svc.sameAuthorityAsOrigin;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof AltService service)) return false;
+ return svc.equals(service.svc);
+ }
+
+ @Override
+ public int hashCode() {
+ return svc.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return svc.pretty();
+ }
+
+ public static Optional create(final Identity id, final Origin origin,
+ final Deadline deadline, final boolean persist) {
+ Objects.requireNonNull(id);
+ Objects.requireNonNull(origin);
+ if (!origin.isSecure()) {
+ return Optional.empty();
+ }
+ return Optional.of(new AltService(id, origin, deadline, persist, true));
+ }
+
+ private static Optional createUnadvertised(final Logger debug,
+ final Identity id, final Origin origin,
+ final HttpConnection conn,
+ final Deadline deadline, final boolean persist) {
+ Objects.requireNonNull(id);
+ Objects.requireNonNull(origin);
+ if (!origin.isSecure()) {
+ return Optional.empty();
+ }
+ final List sniServerNames = AltSvcProcessor.getSNIServerNames(conn);
+ if (sniServerNames == null || sniServerNames.isEmpty()) {
+ if (debug.on()) {
+ debug.log("Skipping unadvertised altsvc creation of %s because connection %s" +
+ " didn't use SNI during connection establishment", id, conn);
+ }
+ return Optional.empty();
+ }
+ return Optional.of(new AltService(id, origin, deadline, persist, false));
+ }
+
+ }
+
+ // A size limited collection which keeps track of unique InvalidAltSvc instances.
+ // Upon reaching a pre-defined size limit, after adding newer entries, the collection
+ // then removes the eldest (the least recently added) entry from the collection.
+ // The implementation of this class is not thread safe and any concurrent access
+ // to instances of this class should be guarded externally.
+ private static final class InvalidAltServices extends LinkedHashMap {
+
+ private static final long serialVersionUID = 2772562283544644819L;
+
+ // we track only a reasonably small number of invalid alt services
+ private static final int MAX_TRACKED_INVALID_ALT_SVCS = 20;
+
+ @Override
+ protected boolean removeEldestEntry(final Map.Entry eldest) {
+ return size() > MAX_TRACKED_INVALID_ALT_SVCS;
+ }
+
+ private boolean contains(final InvalidAltSvc invalidAltSvc) {
+ return this.containsKey(invalidAltSvc);
+ }
+
+ private boolean addUnique(final InvalidAltSvc invalidAltSvc) {
+ if (contains(invalidAltSvc)) {
+ return false;
+ }
+ this.put(invalidAltSvc, null);
+ return true;
+ }
+ }
+
+ // An alt-service is invalid for a particular origin
+ private record InvalidAltSvc(Origin origin, AltService.Identity id) {
+ }
+
+ private boolean keepAltServiceFor(Origin origin, AltService svc) {
+ // skip invalid alt services
+ if (isMarkedInvalid(origin, svc.identity())) {
+ if (debug.on()) {
+ debug.log("Not registering alt-service which was previously" +
+ " marked invalid: " + svc);
+ }
+ if (Log.altsvc()) {
+ Log.logAltSvc("AltService skipped (was previously marked invalid): " + svc);
+ }
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Declare a new Alternate Service endpoint for the given origin.
+ *
+ * @param origin the origin
+ * @param services a set of alt services for the origin
+ */
+ public void replace(final Origin origin, final List services) {
+ Objects.requireNonNull(origin);
+ Objects.requireNonNull(services);
+ List added;
+ registryLock.lock();
+ try {
+ // the list needs to be thread safe to ensure that we won't
+ // get a ConcurrentModificationException when iterating
+ // through the elements in list::stream();
+ added = altServices.compute(origin, (key, list) -> {
+ Stream svcs = services.stream()
+ .filter(AltService.class::isInstance) // filter null
+ .filter((s) -> keepAltServiceFor(origin, s));
+ List newList = svcs.toList();
+ return newList.isEmpty() ? null : newList;
+ });
+ } finally {
+ registryLock.unlock();
+ }
+ if (debug.on()) {
+ debug.log("parsed services: %s", services);
+ debug.log("resulting services: %s", added);
+ }
+ if (Log.altsvc()) {
+ if (added != null) {
+ added.forEach((svc) -> Log.logAltSvc("AltService registry updated: {0}", svc));
+ }
+ }
+ }
+
+ // should be invoked while holding registryLock
+ private boolean isMarkedInvalid(final Origin origin, final AltService.Identity id) {
+ assert registryLock.isHeldByCurrentThread() : "Thread isn't holding registry lock";
+ return this.invalidAltServices.contains(new InvalidAltSvc(origin, id));
+ }
+
+ /**
+ * Registers an unadvertised alt service for the given origin and the alpn.
+ *
+ * @param id The alt service identity
+ * @param origin The origin
+ * @return An {@code Optional} containing the registered {@code AltService},
+ * or {@link Optional#empty()} if the service was not registered.
+ */
+ Optional registerUnadvertised(final AltService.Identity id,
+ final Origin origin,
+ final HttpConnection conn) {
+ Objects.requireNonNull(id);
+ Objects.requireNonNull(origin);
+ registryLock.lock();
+ try {
+ // an unadvertised alt service is registered by an origin only after a
+ // successful connection has completed with that alt service. This effectively means
+ // that we shouldn't check our "invalid alt services" collection, since a successful
+ // connection against the alt service implies a valid alt service.
+ // Additionally, we remove it from the "invalid alt services" collection for this
+ // origin, if at all it was part of that collection
+ this.invalidAltServices.remove(new InvalidAltSvc(origin, id));
+ // default max age as per AltService RFC-7838, section 3.1 is 24 hours. we use
+ // that same value for unadvertised alt-service(s) for an origin.
+ final long defaultMaxAgeInSecs = 3600 * 24;
+ final Deadline deadline = TimeSource.now().plusSeconds(defaultMaxAgeInSecs);
+ final Optional created = AltService.createUnadvertised(debug,
+ id, origin, conn, deadline, true);
+ if (created.isEmpty()) {
+ return Optional.empty();
+ }
+ final AltService altSvc = created.get();
+ altServices.compute(origin, (key, list) -> {
+ Stream old = list == null ? Stream.empty() : list.stream();
+ List newList = Stream.concat(old, Stream.of(altSvc)).toList();
+ return newList.isEmpty() ? null : newList;
+ });
+ if (debug.on()) {
+ debug.log("Added unadvertised AltService: %s", created);
+ }
+ if (Log.altsvc()) {
+ Log.logAltSvc("Added unadvertised AltService: {0}", created);
+ }
+ return created;
+ } finally {
+ registryLock.unlock();
+ }
+ }
+
+ /**
+ * Clear the alternate services of the specified origin from the registry
+ *
+ * @param origin The origin whose alternate services need to be cleared
+ */
+ public void clear(final Origin origin) {
+ Objects.requireNonNull(origin);
+ registryLock.lock();
+ try {
+ if (Log.altsvc()) {
+ Log.logAltSvc("Clearing AltServices for: " + origin);
+ }
+ altServices.remove(origin);
+ } finally {
+ registryLock.unlock();
+ }
+ }
+
+ public void markInvalid(final AltService altService) {
+ Objects.requireNonNull(altService);
+ markInvalid(altService.origin(), altService.identity());
+ }
+
+ private void markInvalid(final Origin origin, final AltService.Identity id) {
+ Objects.requireNonNull(origin);
+ Objects.requireNonNull(id);
+ registryLock.lock();
+ try {
+ // remove this alt service from the current active set of the origin
+ this.altServices.computeIfPresent(origin,
+ (key, currentActive) -> {
+ assert currentActive != null; // should never be null according to spec
+ List newList = currentActive.stream()
+ .filter(Predicate.not(id::matches)).toList();
+ return newList.isEmpty() ? null : newList;
+
+ });
+ // additionally keep track of this as an invalid alt service, so that it cannot be
+ // registered again in the future. Banning is temporary.
+ // Banned alt services may get removed from the set at some point due to
+ // implementation constraints. In which case they may get registered again
+ // and banned again, if connecting to the endpoint fails again.
+ this.invalidAltServices.addUnique(new InvalidAltSvc(origin, id));
+ if (debug.on()) {
+ debug.log("AltService marked invalid: " + id + " for origin " + origin);
+ }
+ if (Log.altsvc()) {
+ Log.logAltSvc("AltService marked invalid: " + id + " for origin " + origin);
+ }
+ } finally {
+ registryLock.unlock();
+ }
+
+ }
+
+ public Stream lookup(final URI uri, final String alpn) {
+ final Origin origin;
+ try {
+ origin = Origin.from(uri);
+ } catch (IllegalArgumentException iae) {
+ return Stream.empty();
+ }
+ return lookup(origin, alpn);
+ }
+
+ /**
+ * A stream of {@code AlternateService} that are available for the
+ * given origin and the given ALPN.
+ *
+ * @param origin the URI of the origin server
+ * @param alpn the ALPN of the alternate service
+ * @return a stream of {@code AlternateService} that are available for the
+ * given origin and that support the given ALPN
+ */
+ public Stream lookup(final Origin origin, final String alpn) {
+ return lookup(origin, Predicate.isEqual(alpn));
+ }
+
+ public Stream lookup(final URI uri,
+ final Predicate super String> alpnMatcher) {
+ final Origin origin;
+ try {
+ origin = Origin.from(uri);
+ } catch (IllegalArgumentException iae) {
+ return Stream.empty();
+ }
+ return lookup(origin, alpnMatcher);
+ }
+
+ private boolean isExpired(AltService service, Deadline now) {
+ var deadline = service.deadline();
+ if (now.equals(deadline) || now.isAfter(deadline)) {
+ // expired, remove from the list
+ if (debug.on()) {
+ debug.log("Removing expired alt-service " + service);
+ }
+ if (Log.altsvc()) {
+ Log.logAltSvc("AltService has expired: {0}", service);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * A stream of {@code AlternateService} that are available for the
+ * given origin and the given ALPN.
+ *
+ * @param origin the URI of the origin server
+ * @param alpnMatcher a predicate to select particular AltService(s) based on the alpn
+ * of the alternate service
+ * @return a stream of {@code AlternateService} that are available for the
+ * given origin and whose ALPN satisfies the {@code alpn} predicate.
+ */
+ private Stream lookup(final Origin origin,
+ final Predicate super String> alpnMatcher) {
+ if (debug.on()) debug.log("looking up alt-service for: %s", origin);
+ final List services;
+ registryLock.lock();
+ try {
+ // we first drop any expired services
+ final Deadline now = TimeSource.now();
+ services = altServices.compute(origin, (key, list) -> {
+ if (list == null) return null;
+ List newList = list.stream()
+ .filter((s) -> !isExpired(s, now))
+ .toList();
+ return newList.isEmpty() ? null : newList;
+ });
+ } finally {
+ registryLock.unlock();
+ }
+ // the order is important - since preferred service are at the head
+ return services == null
+ ? Stream.empty()
+ : services.stream().sequential().filter(s -> alpnMatcher.test(s.identity().alpn()));
+ }
+
+ /**
+ * @param altService The alternate service
+ * {@return true if the {@code service} is known to this registry and the
+ * service isn't past its max age. false otherwise}
+ * @throws NullPointerException if {@code service} is null
+ */
+ public boolean isActive(final AltService altService) {
+ Objects.requireNonNull(altService);
+ return isActive(altService.origin(), altService.identity());
+ }
+
+ private boolean isActive(final Origin origin, final AltService.Identity id) {
+ Objects.requireNonNull(origin);
+ Objects.requireNonNull(id);
+ registryLock.lock();
+ try {
+ final List currentActive = this.altServices.get(origin);
+ if (currentActive == null) {
+ return false;
+ }
+ AltService svc = null;
+ for (AltService s : currentActive) {
+ if (s.identity().equals(id)) {
+ svc = s;
+ break;
+ }
+ }
+ if (svc == null) {
+ return false;
+ }
+ // verify that the service hasn't expired
+ final Deadline now = TimeSource.now();
+ final Deadline deadline = svc.deadline();
+ final boolean expired = now.equals(deadline) || now.isAfter(deadline);
+ if (expired) {
+ // remove from the registry
+ altServices.put(origin, currentActive.stream()
+ .filter(Predicate.not(svc::equals)).toList());
+ if (debug.on()) {
+ debug.log("Removed expired alt-service " + svc + " for origin " + origin);
+ }
+ if (Log.altsvc()) {
+ Log.logAltSvc("Removed AltService: {0}", svc);
+ }
+ return false;
+ }
+ return true;
+ } finally {
+ registryLock.unlock();
+ }
+ }
+}
diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/AltSvcProcessor.java b/src/java.net.http/share/classes/jdk/internal/net/http/AltSvcProcessor.java
new file mode 100644
index 00000000000..b172a242346
--- /dev/null
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/AltSvcProcessor.java
@@ -0,0 +1,495 @@
+/*
+ * Copyright (c) 2020, 2025, 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. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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.internal.net.http;
+
+import jdk.internal.net.http.AltServicesRegistry.AltService;
+import jdk.internal.net.http.common.Deadline;
+import jdk.internal.net.http.common.Log;
+import jdk.internal.net.http.common.Logger;
+import jdk.internal.net.http.common.TimeSource;
+import jdk.internal.net.http.common.Utils;
+import jdk.internal.net.http.frame.AltSvcFrame;
+
+import java.net.InetSocketAddress;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.StringTokenizer;
+
+import javax.net.ssl.SNIHostName;
+import javax.net.ssl.SNIServerName;
+
+import static jdk.internal.net.http.Http3ClientProperties.ALTSVC_ALLOW_LOCAL_HOST_ORIGIN;
+import static jdk.internal.net.http.common.Alpns.isSecureALPNName;
+
+
+/**
+ * Responsible for parsing the Alt-Svc values from an Alt-Svc header and/or AltSvc HTTP/2 frame.
+ */
+final class AltSvcProcessor {
+
+ private static final String HEADER = "alt-svc";
+ private static final Logger debug = Utils.getDebugLogger(() -> "AltSvc");
+ // a special value that we return back while parsing the header values,
+ // indicate that all existing alternate services for a origin need to be cleared
+ private static final List CLEAR_ALL_ALT_SVCS = List.of();
+ // whether or not alt service can be created from "localhost" origin host
+ private static final boolean allowLocalHostOrigin = ALTSVC_ALLOW_LOCAL_HOST_ORIGIN;
+
+ private static final SNIHostName LOCALHOST_SNI = new SNIHostName("localhost");
+
+ private record ParsedHeaderValue(String rawValue, String alpnName, String host, int port,
+ Map parameters) {
+ }
+
+ private AltSvcProcessor() {
+ throw new UnsupportedOperationException("Instantiation not supported");
+ }
+
+
+ /**
+ * Parses the alt-svc header received from origin and update
+ * registry with the processed values.
+ *
+ * @param response response passed on by the server
+ * @param client client that holds alt-svc registry
+ * @param request request that holds the origin details
+ */
+ static void processAltSvcHeader(Response response, HttpClientImpl client,
+ HttpRequestImpl request) {
+
+ // we don't support AltSvc from unsecure origins
+ if (!request.secure()) {
+ return;
+ }
+ if (response.statusCode == 421) {
+ // As per AltSvc spec (RFC-7838), section 6:
+ // An Alt-Svc header field in a 421 (Misdirected Request) response MUST be ignored.
+ return;
+ }
+ final var altSvcHeaderVal = response.headers().firstValue(HEADER);
+ if (altSvcHeaderVal.isEmpty()) {
+ return;
+ }
+ if (debug.on()) {
+ debug.log("Processing alt-svc header");
+ }
+ final HttpConnection conn = response.exchange.exchImpl.connection();
+ final List sniServerNames = getSNIServerNames(conn);
+ if (sniServerNames.isEmpty()) {
+ // we don't trust the alt-svc advertisement if the connection over which it
+ // was advertised didn't use SNI during TLS handshake while establishing the connection
+ if (debug.on()) {
+ debug.log("ignoring alt-svc header because connection %s didn't use SNI during" +
+ " connection establishment", conn);
+ }
+ return;
+ }
+ final Origin origin;
+ try {
+ origin = Origin.from(request.uri());
+ } catch (IllegalArgumentException iae) {
+ if (debug.on()) {
+ debug.log("ignoring alt-svc header due to: " + iae);
+ }
+ // ignore the alt-svc
+ return;
+ }
+ String altSvcValue = altSvcHeaderVal.get();
+ processValueAndUpdateRegistry(client, origin, altSvcValue);
+ }
+
+ static void processAltSvcFrame(final int streamId,
+ final AltSvcFrame frame,
+ final HttpConnection conn,
+ final HttpClientImpl client) {
+ final String value = frame.getAltSvcValue();
+ if (value == null || value.isBlank()) {
+ return;
+ }
+ if (!conn.isSecure()) {
+ // don't support alt svc from unsecure origins
+ return;
+ }
+ final List sniServerNames = getSNIServerNames(conn);
+ if (sniServerNames.isEmpty()) {
+ // we don't trust the alt-svc advertisement if the connection over which it
+ // was advertised didn't use SNI during TLS handshake while establishing the connection
+ if (debug.on()) {
+ debug.log("ignoring altSvc frame because connection %s didn't use SNI during" +
+ " connection establishment", conn);
+ }
+ return;
+ }
+ debug.log("processing AltSvcFrame %s", value);
+ final Origin origin;
+ if (streamId == 0) {
+ // section 4, RFC-7838 - alt-svc frame on stream 0 with empty (zero length) origin
+ // is invalid and MUST be ignored
+ if (frame.getOrigin().isBlank()) {
+ // invalid frame, ignore it
+ debug.log("Ignoring invalid alt-svc frame on stream 0 of " + conn);
+ return;
+ }
+ // parse origin from frame.getOrigin() string which is in ASCII
+ // serialized form of an origin (defined in section 6.2 of RFC-6454)
+ final Origin parsedOrigin;
+ try {
+ parsedOrigin = Origin.fromASCIISerializedForm(frame.getOrigin());
+ } catch (IllegalArgumentException iae) {
+ // invalid origin value, ignore the frame
+ debug.log("origin couldn't be parsed, ignoring invalid alt-svc frame" +
+ " on stream " + streamId + " of " + conn);
+ return;
+ }
+ // currently we do not allow an alt service to be advertised for a different origin.
+ // if the origin advertised in the alt-svc frame doesn't match the origin of the
+ // connection, then we ignore it. the RFC allows us to do that:
+ // RFC-7838, section 4:
+ // An ALTSVC frame from a server to a client on stream 0 indicates that
+ // the conveyed alternative service is associated with the origin
+ // contained in the Origin field of the frame. An association with an
+ // origin that the client does not consider authoritative for the
+ // current connection MUST be ignored.
+ if (!parsedOrigin.equals(conn.getOriginServer())) {
+ debug.log("ignoring alt-svc frame on stream 0 for origin: " + parsedOrigin
+ + " received on connection of origin: " + conn.getOriginServer());
+ return;
+ }
+ origin = parsedOrigin;
+ } else {
+ // (section 4, RFC-7838) - for non-zero stream id, the alt-svc is for the origin of
+ // the stream. Additionally, an ALTSVC frame on a stream other than stream 0 containing
+ // non-empty "Origin" information is invalid and MUST be ignored.
+ if (!frame.getOrigin().isEmpty()) {
+ // invalid frame, ignore it
+ debug.log("non-empty origin in alt-svc frame on stream " + streamId + " of "
+ + conn + ", ignoring alt-svc frame");
+ return;
+ }
+ origin = conn.getOriginServer();
+ assert origin != null : "origin server is null on connection: " + conn;
+ }
+ processValueAndUpdateRegistry(client, origin, value);
+ }
+
+ private static void processValueAndUpdateRegistry(HttpClientImpl client,
+ Origin origin,
+ String altSvcValue) {
+ final List altServices = processHeaderValue(origin, altSvcValue);
+ // intentional identity check
+ if (altServices == CLEAR_ALL_ALT_SVCS) {
+ // clear all existing alt services for this origin
+ debug.log("clearing AltServiceRegistry for " + origin);
+ client.registry().clear(origin);
+ return;
+ }
+ debug.log("AltServices: %s", altServices);
+ if (altServices.isEmpty()) {
+ return;
+ }
+ // AltService RFC-7838, section 3.1 states:
+ //
+ // When an Alt-Svc response header field is received from an origin, its
+ // value invalidates and replaces all cached alternative services for
+ // that origin.
+ client.registry().replace(origin, altServices);
+ }
+
+ static List getSNIServerNames(final HttpConnection conn) {
+ final List sniServerNames = conn.getSNIServerNames();
+ if (sniServerNames != null && !sniServerNames.isEmpty()) {
+ return sniServerNames;
+ }
+ // no SNI server name(s) were used when establishing this connection. check if
+ // this connection is to a loopback address and if it is then see if a configuration
+ // has been set to allow alt services advertised by loopback addresses to be trusted/accepted.
+ // if such a configuration has been set, then we return a SNIHostName for "localhost"
+ final InetSocketAddress addr = conn.address();
+ final boolean isLoopbackAddr = addr.isUnresolved()
+ ? false
+ : conn.address.getAddress().isLoopbackAddress();
+ if (!isLoopbackAddr) {
+ return List.of(); // no SNI server name(s) used for this connection
+ }
+ if (!allowLocalHostOrigin) {
+ // this is a connection to a loopback address, with no SNI server name(s) used
+ // during TLS handshake and the configuration doesn't allow accepting/trusting
+ // alt services from loopback address, so we return no SNI server name(s) for this
+ // connection
+ return List.of();
+ }
+ // at this point, we have identified this as a loopback address and the configuration
+ // has been set to accept/trust alt services from loopback address, so we return a
+ // SNIHostname corresponding to "localhost"
+ return List.of(LOCALHOST_SNI);
+ }
+
+ // Here are five examples of values for the Alt-Svc header:
+ // String svc1 = """foo=":443"; ma=2592000; persist=1"""
+ // String svc2 = """h3="localhost:5678"""";
+ // String svc3 = """bar3=":446"; ma=2592000; persist=1""";
+ // String svc4 = """h3-34=":5678"; ma=2592000; persist=1""";
+ // String svc5 = "%s, %s, %s, %s".formatted(svc1, svc2, svc3, svc4);
+ // The last one (svc5) should result in two services being registered:
+ // AltService[origin=https://localhost:64077/, alpn=h3, endpoint=localhost/127.0.0.1:5678,
+ // deadline=2021-03-13T01:41:01.369488Z, persist=false]
+ // AltService[origin=https://localhost:64077/, alpn=h3-34, endpoint=localhost/127.0.0.1:5678,
+ // deadline=2021-04-11T01:41:01.369912Z, persist=true]
+ private static List processHeaderValue(final Origin origin,
+ final String headerValue) {
+ final List altServices = new ArrayList<>();
+ // multiple alternate services can be specified with comma as a delimiter
+ final var altSvcs = headerValue.split(",");
+ for (var altSvc : altSvcs) {
+ altSvc = altSvc.trim();
+
+ // each value is expected to be of the following form, as noted in RFC-7838, section 3
+ // Alt-Svc = clear / 1#alt-value
+ // clear = %s"clear"; "clear", case-sensitive
+ // alt-value = alternative *( OWS ";" OWS parameter )
+ // alternative = protocol-id "=" alt-authority
+ // protocol-id = token ; percent-encoded ALPN protocol name
+ // alt-authority = quoted-string ; containing [ uri-host ] ":" port
+ // parameter = token "=" ( token / quoted-string )
+
+ // As per the spec, the value "clear" is expected to be case-sensitive
+ if (altSvc.equals("clear")) {
+ return CLEAR_ALL_ALT_SVCS;
+ }
+ final ParsedHeaderValue parsed = parseAltValue(origin, altSvc);
+ if (parsed == null) {
+ // this implies the alt-svc header value couldn't be parsed and thus is malformed.
+ // we skip such header values.
+ debug.log("skipping %s", altSvc);
+ continue;
+ }
+ final var deadline = getValidTill(parsed.parameters().get("ma"));
+ final var persist = getPersist(parsed.parameters().get("persist"));
+ final AltService.Identity altSvcId = new AltService.Identity(parsed.alpnName(),
+ parsed.host(), parsed.port());
+ AltService.create(altSvcId, origin, deadline, persist)
+ .ifPresent((altsvc) -> {
+ altServices.add(altsvc);
+ if (Log.altsvc()) {
+ final var s = altsvc;
+ Log.logAltSvc("Created AltService: {0}", s);
+ } else if (debug.on()) {
+ debug.log("Created AltService for id=%s, origin=%s%n", altSvcId, origin);
+ }
+ });
+ }
+ return altServices;
+ }
+
+ private static ParsedHeaderValue parseAltValue(final Origin origin, final String altValue) {
+ // header value is expected to be of the following form, as noted in RFC-7838, section 3
+ // Alt-Svc = clear / 1#alt-value
+ // clear = %s"clear"; "clear", case-sensitive
+ // alt-value = alternative *( OWS ";" OWS parameter )
+ // alternative = protocol-id "=" alt-authority
+ // protocol-id = token ; percent-encoded ALPN protocol name
+ // alt-authority = quoted-string ; containing [ uri-host ] ":" port
+ // parameter = token "=" ( token / quoted-string )
+
+ // find the = sign that separates the protocol-id and alt-authority
+ debug.log("parsing %s", altValue);
+ final int alternativeDelimIndex = altValue.indexOf("=");
+ if (alternativeDelimIndex == -1 || alternativeDelimIndex == altValue.length() - 1) {
+ // not a valid alt value
+ debug.log("no \"=\" character in %s", altValue);
+ return null;
+ }
+ // key is always the protocol-id. example, in 'h3="localhost:5678"; ma=23232; persist=1'
+ // "h3" acts as the key with '"localhost:5678"; ma=23232; persist=1' as the value
+ final String protocolId = altValue.substring(0, alternativeDelimIndex);
+ // the protocol-id can be percent encoded as per the spec, so we decode it to get the alpn name
+ final var alpnName = decodePotentialPercentEncoded(protocolId);
+ debug.log("alpn is %s in %s", alpnName, altValue);
+ if (!isSecureALPNName(alpnName)) {
+ // no reasonable assurance that the alternate service will be under the control
+ // of the origin (section 2.1, RFC-7838)
+ debug.log("alpn %s is not secure, skipping", alpnName);
+ return null;
+ }
+ String remaining = altValue.substring(alternativeDelimIndex + 1);
+ // now parse alt-authority
+ if (!remaining.startsWith("\"") || remaining.length() == 1) {
+ // we expect a quoted string for alt-authority
+ debug.log("no quoted authority in %s", altValue);
+ return null;
+ }
+ remaining = remaining.substring(1); // skip the starting double quote
+ final int nextDoubleQuoteIndex = remaining.indexOf("\"");
+ if (nextDoubleQuoteIndex == -1) {
+ // malformed value
+ debug.log("missing closing quote in %s", altValue);
+ return null;
+ }
+ final String altAuthority = remaining.substring(0, nextDoubleQuoteIndex);
+ final HostPort hostPort = getHostPort(origin, altAuthority);
+ if (hostPort == null) return null; // host port could not be parsed
+ if (nextDoubleQuoteIndex == remaining.length() - 1) {
+ // there's nothing more left to parse
+ return new ParsedHeaderValue(altValue, alpnName, hostPort.host(), hostPort.port(), Map.of());
+ }
+ // parse the semicolon delimited parameters out of the rest of the remaining string
+ remaining = remaining.substring(nextDoubleQuoteIndex + 1);
+ final Map parameters = extractParameters(remaining);
+ return new ParsedHeaderValue(altValue, alpnName, hostPort.host(), hostPort.port(), parameters);
+ }
+
+ private static String decodePotentialPercentEncoded(final String val) {
+ if (!val.contains("%")) {
+ return val;
+ }
+ // TODO: impl this
+ // In practice this method is only used for the ALPN.
+ // We only support h3 for now, so we do not need to
+ // decode percents: anything else but h3 will eventually be ignored.
+ return val;
+ }
+
+ private static Map extractParameters(final String val) {
+ // As per the spec, parameters take the form of:
+ // *( OWS ";" OWS parameter )
+ // ...
+ // parameter = token "=" ( token / quoted-string )
+ //
+ // where * represents "any number of" and OWS means "optional whitespace"
+
+ final var tokenizer = new StringTokenizer(val, ";");
+ if (!tokenizer.hasMoreTokens()) {
+ return Map.of();
+ }
+ Map parameters = null;
+ while (tokenizer.hasMoreTokens()) {
+ final var parameter = tokenizer.nextToken().trim();
+ if (parameter.isEmpty()) {
+ continue;
+ }
+ final var equalSignIndex = parameter.indexOf('=');
+ if (equalSignIndex == -1 || equalSignIndex == parameter.length() - 1) {
+ // a parameter is expected to have a "=" delimiter which separates a key and a value.
+ // we skip parameters which don't conform to that rule
+ continue;
+ }
+ final var paramKey = parameter.substring(0, equalSignIndex);
+ final var paramValue = parameter.substring(equalSignIndex + 1);
+ if (parameters == null) {
+ parameters = new HashMap<>();
+ }
+ parameters.put(paramKey, paramValue);
+ }
+ if (parameters == null) {
+ return Map.of();
+ }
+ return Collections.unmodifiableMap(parameters);
+ }
+
+ private record HostPort(String host, int port) {}
+
+ private static HostPort getHostPort(Origin origin, String altAuthority) {
+ // The AltService spec defines an alt-authority as follows:
+ //
+ // alt-authority = quoted-string ; containing [ uri-host ] ":" port
+ //
+ // When this method is called the passed altAuthority is already stripped of the leading and trailing
+ // double-quotes. The value will this be of the form [uri-host]:port where uri-host is optional.
+ String host; int port;
+ try {
+ // Use URI to do the parsing, with a special case for optional host
+ URI uri = new URI("http://" + altAuthority + "/");
+ host = uri.getHost();
+ port = uri.getPort();
+ if (host == null && port == -1) {
+ var auth = uri.getRawAuthority();
+ if (auth.isEmpty()) return null;
+ if (auth.charAt(0) == ':') {
+ uri = new URI("http://x" + altAuthority + "/");
+ if ("x".equals(uri.getHost())) {
+ port = uri.getPort();
+ }
+ }
+ }
+ if (port == -1) {
+ debug.log("Can't parse authority: " + altAuthority);
+ return null;
+ }
+ String hostport;
+ if (host == null || host.isEmpty()) {
+ hostport = ":" + port;
+ host = origin.host();
+ } else {
+ hostport = host + ":" + port;
+ }
+ // reject anything unexpected. altAuthority should match hostport
+ if (!hostport.equals(altAuthority)) {
+ debug.log("Authority \"%s\" doesn't match host:port \"%s\"",
+ altAuthority, hostport);
+ return null;
+ }
+ } catch (URISyntaxException x) {
+ debug.log("Failed to parse authority: %s - %s",
+ altAuthority, x);
+ return null;
+ }
+ return new HostPort(host, port);
+ }
+
+ private static Deadline getValidTill(final String maxAge) {
+ // There's a detailed algorithm in RFC-7234 section 4.2.3, for calculating the age. This
+ // RFC section is referenced from the alternate service RFC-7838 section 3.1.
+ // For now though, we use "now" as the instant against which the age will be applied.
+ final Deadline responseGenerationInstant = TimeSource.now();
+ // default max age as per AltService RFC-7838, section 3.1 is 24 hours
+ final long defaultMaxAgeInSecs = 3600 * 24;
+ if (maxAge == null) {
+ return responseGenerationInstant.plusSeconds(defaultMaxAgeInSecs);
+ }
+ try {
+ final long seconds = Long.parseLong(maxAge);
+ // negative values aren't allowed for max-age as per RFC-7234, section 1.2.1
+ return seconds < 0 ? responseGenerationInstant.plusSeconds(defaultMaxAgeInSecs)
+ : responseGenerationInstant.plusSeconds(seconds);
+ } catch (NumberFormatException nfe) {
+ return responseGenerationInstant.plusSeconds(defaultMaxAgeInSecs);
+ }
+ }
+
+ private static boolean getPersist(final String persist) {
+ // AltService RFC-7838, section 3.1, states:
+ //
+ // This specification only defines a single value for "persist".
+ // Clients MUST ignore "persist" parameters with values other than "1".
+ //
+ return "1".equals(persist);
+ }
+}
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 1ee54ed2bef..c50a4922e80 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
@@ -27,6 +27,7 @@ package jdk.internal.net.http;
import java.io.IOException;
import java.net.ProtocolException;
+import java.net.http.HttpClient.Version;
import java.time.Duration;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
@@ -62,6 +63,7 @@ final class Exchange {
volatile ExchangeImpl exchImpl;
volatile CompletableFuture extends ExchangeImpl> exchangeCF;
volatile CompletableFuture bodyIgnored;
+ volatile boolean streamLimitReached;
// used to record possible cancellation raised before the exchImpl
// has been established.
@@ -74,11 +76,18 @@ final class Exchange {
final String dbgTag;
// Keeps track of the underlying connection when establishing an HTTP/2
- // exchange so that it can be aborted/timed out mid setup.
+ // or HTTP/3 exchange so that it can be aborted/timed out mid-setup.
final ConnectionAborter connectionAborter = new ConnectionAborter();
final AtomicInteger nonFinalResponses = new AtomicInteger();
+ // This will be set to true only when it is guaranteed that the server hasn't processed
+ // the request. Typically, this happens when the server explicitly states (through a GOAWAY frame
+ // or a relevant error code in reset frame) that the corresponding stream (id) wasn't processed.
+ // However, there can be cases where the client is certain that the request wasn't sent
+ // to the server (and thus not processed). In such cases, the client can set this to true.
+ private volatile boolean unprocessedByPeer;
+
Exchange(HttpRequestImpl request, MultiExchange multi) {
this.request = request;
this.upgrading = false;
@@ -110,9 +119,13 @@ final class Exchange {
}
// Keeps track of the underlying connection when establishing an HTTP/2
- // exchange so that it can be aborted/timed out mid setup.
- static final class ConnectionAborter {
+ // or HTTP/3 exchange so that it can be aborted/timed out mid setup.
+ final class ConnectionAborter {
+ // In case of HTTP/3 requests we may have
+ // two connections in parallel: a regular TCP connection
+ // and a QUIC connection.
private volatile HttpConnection connection;
+ private volatile HttpQuicConnection quicConnection;
private volatile boolean closeRequested;
private volatile Throwable cause;
@@ -123,10 +136,11 @@ final class Exchange {
// closed
closeRequested = this.closeRequested;
if (!closeRequested) {
- this.connection = connection;
- } else {
- // assert this.connection == null
- this.closeRequested = false;
+ if (connection instanceof HttpQuicConnection quicConnection) {
+ this.quicConnection = quicConnection;
+ } else {
+ this.connection = connection;
+ }
}
}
if (closeRequested) closeConnection(connection, cause);
@@ -134,6 +148,7 @@ final class Exchange {
void closeConnection(Throwable error) {
HttpConnection connection;
+ HttpQuicConnection quicConnection;
Throwable cause;
synchronized (this) {
cause = this.cause;
@@ -141,39 +156,64 @@ final class Exchange {
cause = error;
}
connection = this.connection;
- if (connection == null) {
+ quicConnection = this.quicConnection;
+ if (connection == null || quicConnection == null) {
closeRequested = true;
this.cause = cause;
} else {
+ this.quicConnection = null;
this.connection = null;
this.cause = null;
}
}
closeConnection(connection, cause);
+ closeConnection(quicConnection, cause);
}
+ // Called by HTTP/2 after an upgrade.
+ // There is no upgrade for HTTP/3
HttpConnection disable() {
HttpConnection connection;
synchronized (this) {
connection = this.connection;
this.connection = null;
+ this.quicConnection = null;
this.closeRequested = false;
this.cause = null;
}
return connection;
}
- private static void closeConnection(HttpConnection connection, Throwable cause) {
- if (connection != null) {
- try {
- connection.close(cause);
- } catch (Throwable t) {
- // ignore
+ void clear(HttpConnection connection) {
+ synchronized (this) {
+ var c = this.connection;
+ if (connection == c) this.connection = null;
+ var qc = this.quicConnection;
+ if (connection == qc) this.quicConnection = null;
+ }
+ }
+
+ private void closeConnection(HttpConnection connection, Throwable cause) {
+ if (connection == null) {
+ return;
+ }
+ try {
+ connection.close(cause);
+ } catch (Throwable t) {
+ // ignore
+ if (debug.on()) {
+ debug.log("ignoring exception that occurred during closing of connection: "
+ + connection, t);
}
}
}
}
+ // true if previous attempt resulted in streamLimitReached
+ public boolean hasReachedStreamLimit() { return streamLimitReached; }
+ // can be used to set or clear streamLimitReached (for instance clear it after retrying)
+ void streamLimitReached(boolean streamLimitReached) { this.streamLimitReached = streamLimitReached; }
+
// Called for 204 response - when no body is permitted
// This is actually only needed for HTTP/1.1 in order
// to return the connection to the pool (or close it)
@@ -253,7 +293,7 @@ final class Exchange {
impl.cancel(cause);
} else {
// abort/close the connection if setting up the exchange. This can
- // be important when setting up HTTP/2
+ // be important when setting up HTTP/2 or HTTP/3
closeReason = failed.get();
if (closeReason != null) {
connectionAborter.closeConnection(closeReason);
@@ -283,6 +323,9 @@ final class Exchange {
cf = exchangeCF;
}
}
+ if (multi.requestCancelled() && impl != null && cause == null) {
+ cause = new IOException("Request cancelled");
+ }
if (cause == null) return;
if (impl != null) {
// The exception is raised by propagating it to the impl.
@@ -314,7 +357,7 @@ final class Exchange {
// if upgraded, we don't close the connection.
// cancelling will be handled by the HTTP/2 exchange
// in its own time.
- if (!upgraded) {
+ if (!upgraded && !(connection instanceof HttpQuicConnection)) {
t = getCancelCause();
if (t == null) t = new IOException("Request cancelled");
if (debug.on()) debug.log("exchange cancelled during connect: " + t);
@@ -350,8 +393,8 @@ final class Exchange {
private CompletableFuture extends ExchangeImpl>
establishExchange(HttpConnection connection) {
if (debug.on()) {
- debug.log("establishing exchange for %s,%n\t proxy=%s",
- request, request.proxy());
+ debug.log("establishing exchange for %s #%s,%n\t proxy=%s",
+ request, multi.id, request.proxy());
}
// check if we have been cancelled first.
Throwable t = getCancelCause();
@@ -364,7 +407,17 @@ final class Exchange {
}
CompletableFuture extends ExchangeImpl> cf, res;
- cf = ExchangeImpl.get(this, connection);
+
+ cf = ExchangeImpl.get(this, connection)
+ // set exchImpl and call checkCancelled to make sure exchImpl
+ // gets cancelled even if the exchangeCf was completed exceptionally
+ // before the CF returned by ExchangeImpl.get completed. This deals
+ // with issues when the request is cancelled while the exchange impl
+ // is being created.
+ .thenApply((eimpl) -> {
+ synchronized (Exchange.this) {exchImpl = eimpl;}
+ checkCancelled(); return eimpl;
+ }).copy();
// We should probably use a VarHandle to get/set exchangeCF
// instead - as we need CAS semantics.
synchronized (this) { exchangeCF = cf; };
@@ -390,7 +443,7 @@ final class Exchange {
}
// Completed HttpResponse will be null if response succeeded
- // will be a non null responseAsync if expect continue returns an error
+ // will be a non-null responseAsync if expect continue returns an error
public CompletableFuture responseAsync() {
return responseAsyncImpl(null);
@@ -715,4 +768,13 @@ final class Exchange {
String dbgString() {
return dbgTag;
}
+
+ final boolean isUnprocessedByPeer() {
+ return this.unprocessedByPeer;
+ }
+
+ // Marks the exchange as unprocessed by the peer
+ final void markUnprocessedByPeer() {
+ this.unprocessedByPeer = true;
+ }
}
diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/ExchangeImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/ExchangeImpl.java
index f393b021cd4..74600e78557 100644
--- a/src/java.net.http/share/classes/jdk/internal/net/http/ExchangeImpl.java
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/ExchangeImpl.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2025, 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
@@ -26,17 +26,26 @@
package jdk.internal.net.http;
import java.io.IOException;
+import java.net.ConnectException;
+import java.net.InetSocketAddress;
+import java.net.http.HttpConnectTimeoutException;
+import java.net.http.HttpOption.Http3DiscoveryMode;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.ResponseInfo;
+import java.net.http.UnsupportedProtocolVersionException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
+import java.util.function.Supplier;
+import jdk.internal.net.http.Http2Connection.ALPNException;
import jdk.internal.net.http.common.HttpBodySubscriberWrapper;
import jdk.internal.net.http.common.Logger;
import jdk.internal.net.http.common.MinimalFuture;
import jdk.internal.net.http.common.Utils;
import static java.net.http.HttpClient.Version.HTTP_1_1;
+import static java.net.http.HttpClient.Version.HTTP_2;
+import static java.net.http.HttpClient.Version.HTTP_3;
/**
* Splits request so that headers and body can be sent separately with optional
@@ -60,10 +69,6 @@ abstract class ExchangeImpl {
private volatile boolean expectTimeoutRaised;
- // this will be set to true only when the peer explicitly states (through a GOAWAY frame or
- // a relevant error code in reset frame) that the corresponding stream (id) wasn't processed
- private volatile boolean unprocessedByPeer;
-
ExchangeImpl(Exchange e) {
// e == null means a http/2 pushed stream
this.exchange = e;
@@ -98,23 +103,414 @@ abstract class ExchangeImpl {
static CompletableFuture extends ExchangeImpl>
get(Exchange exchange, HttpConnection connection)
{
- if (exchange.version() == HTTP_1_1) {
+ HttpRequestImpl request = exchange.request();
+ var version = exchange.version();
+ if (version == HTTP_1_1 || request.isWebSocket()) {
if (debug.on())
debug.log("get: HTTP/1.1: new Http1Exchange");
return createHttp1Exchange(exchange, connection);
- } else {
- Http2ClientImpl c2 = exchange.client().client2(); // #### improve
- HttpRequestImpl request = exchange.request();
- CompletableFuture c2f = c2.getConnectionFor(request, exchange);
+ } else if (!request.secure() && request.isHttp3Only(version)) {
+ assert version == HTTP_3;
+ assert !request.isWebSocket();
if (debug.on())
- debug.log("get: Trying to get HTTP/2 connection");
- // local variable required here; see JDK-8223553
- CompletableFuture>> fxi =
- c2f.handle((h2c, t) -> createExchangeImpl(h2c, t, exchange, connection));
- return fxi.thenCompose(x->x);
+ debug.log("get: HTTP/3: HTTP/3 is not supported on plain connections");
+ return MinimalFuture.failedFuture(
+ new UnsupportedProtocolVersionException(
+ "HTTP/3 is not supported on plain connections"));
+ } else if (version == HTTP_2 || isTCP(connection) || !request.secure()) {
+ assert !request.isWebSocket();
+ return attemptHttp2Exchange(exchange, connection);
+ } else {
+ assert request.secure();
+ assert version == HTTP_3;
+ assert !request.isWebSocket();
+ return attemptHttp3Exchange(exchange, connection);
}
}
+ private static boolean isTCP(HttpConnection connection) {
+ if (connection instanceof HttpQuicConnection) return false;
+ if (connection == null) return false;
+ // if it's not an HttpQuicConnection and it's not null it's
+ // a TCP connection
+ return true;
+ }
+
+ private static CompletableFuture extends ExchangeImpl>
+ attemptHttp2Exchange(Exchange exchange, HttpConnection connection) {
+ HttpRequestImpl request = exchange.request();
+ Http2ClientImpl c2 = exchange.client().client2(); // #### improve
+ CompletableFuture c2f = c2.getConnectionFor(request, exchange);
+ if (debug.on())
+ debug.log("get: Trying to get HTTP/2 connection");
+ // local variable required here; see JDK-8223553
+ CompletableFuture>> fxi =
+ c2f.handle((h2c, t) -> createExchangeImpl(h2c, t, exchange, connection));
+ return fxi.thenCompose(x -> x);
+ }
+
+ private static CompletableFuture extends ExchangeImpl>
+ attemptHttp3Exchange(Exchange exchange, HttpConnection connection) {
+ HttpRequestImpl request = exchange.request();
+ var exchvers = exchange.version();
+ assert request.secure() : request.uri() + " is not secure";
+ assert exchvers == HTTP_3 : "expected HTTP/3, got " + exchvers;
+ // when we reach here, it's guaranteed that the client supports HTTP3
+ assert exchange.client().client3().isPresent() : "HTTP3 isn't supported by the client";
+ var client3 = exchange.client().client3().get();
+ CompletableFuture c3f;
+ Supplier> c2fs;
+ var config = request.http3Discovery();
+
+ if (debug.on()) {
+ debug.log("get: Trying to get HTTP/3 connection; config is %s", config);
+ }
+ // The algorithm here depends on whether HTTP/3 is specified on
+ // the request itself, or on the HttpClient.
+ // In both cases, we may attempt a direct HTTP/3 connection if
+ // we don't have an H3 endpoint registered in the AltServicesRegistry.
+ // However, if HTTP/3 is not specified explicitly on the request,
+ // we will start both an HTTP/2 and an HTTP/3 connection at the
+ // same time, and use the one that complete first. If HTTP/3 is
+ // specified on the request, we will give priority to HTTP/3 ond
+ // only start the HTTP/2 connection if the HTTP/3 connection fails,
+ // or doesn't succeed in the imparted timeout. The timeout can be
+ // specified with the property "jdk.httpclient.http3.maxDirectConnectionTimeout".
+ // If unspecified it defaults to 2750ms.
+ //
+ // Because the HTTP/2 connection may start as soon as we create the
+ // CompletableFuture returned by the Http2Client,
+ // we are using a Supplier> to
+ // set up the call chain that would start the HTTP/2 connection.
+ try {
+ // first look to see if we already have an HTTP/3 connection in
+ // the pool. If we find one, we're almost done! We won't need
+ // to start any HTTP/2 connection.
+ Http3Connection pooled = client3.findPooledConnectionFor(request, exchange);
+ if (pooled != null) {
+ c3f = MinimalFuture.completedFuture(pooled);
+ c2fs = null;
+ } else {
+ if (debug.on())
+ debug.log("get: no HTTP/3 pooled connection found");
+ // possibly start an HTTP/3 connection
+ boolean mayAttemptDirectConnection = client3.mayAttemptDirectConnection(request);
+ c3f = client3.getConnectionFor(request, exchange);
+ if ((!c3f.isDone() || c3f.isCompletedExceptionally()) && mayAttemptDirectConnection) {
+ // We don't know if the server supports HTTP/3.
+ // happy eyeball: prepare to try both HTTP/3 and HTTP/2 and
+ // to use the first that succeeds
+ if (config != Http3DiscoveryMode.HTTP_3_URI_ONLY) {
+ if (debug.on()) {
+ debug.log("get: trying with both HTTP/3 and HTTP/2");
+ }
+ Http2ClientImpl client2 = exchange.client().client2();
+ c2fs = () -> client2.getConnectionFor(request, exchange);
+ } else {
+ if (debug.on()) {
+ debug.log("get: trying with HTTP/3 only");
+ }
+ c2fs = null;
+ }
+ } else {
+ // We have a completed Http3Connection future.
+ // No need to attempt direct HTTP/3 connection.
+ c2fs = null;
+ }
+ }
+ } catch (IOException io) {
+ return MinimalFuture.failedFuture(io);
+ }
+ if (c2fs == null) {
+ // Do not attempt a happy eyeball: go the normal route to
+ // attempt an HTTP/3 connection
+ // local variable required here; see JDK-8223553
+ if (debug.on()) debug.log("No HTTP/3 eyeball needed");
+ CompletableFuture>> fxi =
+ c3f.handle((h3c, t) -> createExchangeImpl(h3c, t, exchange, connection));
+ return fxi.thenCompose(x->x);
+ } else if (request.version().orElse(null) == HTTP_3) {
+ // explicit request to use HTTP/3, only use HTTP/2 if HTTP/3 fails, but
+ // still start both connections in parallel. HttpQuicConnection will
+ // attempt a direct connection. Because we register
+ // firstToComplete as a dependent action of c3f we will actually
+ // only use HTTP/2 (or HTTP/1.1) if HTTP/3 failed
+ CompletableFuture>> fxi =
+ c3f.handle((h3c, e) -> firstToComplete(exchange, connection, c2fs, c3f));
+ if (debug.on()) {
+ debug.log("Explicit HTTP/3 request: " +
+ "attempt HTTP/3 first, then default to HTTP/2");
+ }
+ return fxi.thenCompose(x->x);
+ }
+ if (debug.on()) {
+ debug.log("Attempt HTTP/3 and HTTP/2 in parallel, use the first that connects");
+ }
+ // default client version is HTTP/3 - request version is not set.
+ // so try HTTP/3 + HTTP/2 in parallel and take the first that completes.
+ return firstToComplete(exchange, connection, c2fs, c3f);
+ }
+
+ // Use the first connection that successfully completes.
+ // This is a bit hairy because HTTP/2 may be downgraded to HTTP/1 if the server
+ // doesn't support HTTP/2. In which case the connection attempt will succeed but
+ // c2f will be completed with a ALPNException.
+ private static CompletableFuture extends ExchangeImpl> firstToComplete(
+ Exchange exchange,
+ HttpConnection connection,
+ Supplier> c2fs,
+ CompletableFuture c3f) {
+ if (debug.on()) {
+ debug.log("firstToComplete(connection=%s)", connection);
+ debug.log("Will use the first connection that succeeds from HTTP/2 or HTTP/3");
+ }
+ assert connection == null : "should not come here if connection is not null: " + connection;
+
+ // Set up a completable future (cf) that will complete
+ // when the first HTTP/3 or HTTP/2 connection result is
+ // available. Error cases (when the result is exceptional)
+ // is handled in a dependent action of cf later below
+ final CompletableFuture> cf;
+ // c3f is used for HTTP/3, c2f for HTTP/2
+ final CompletableFuture c2f;
+ if (c3f.isDone()) {
+ // We already have a result for HTTP/3, consider that first;
+ // There's no need to start HTTP/2 yet if the result is successful.
+ c2f = null;
+ cf = c3f;
+ } else {
+ // No result for HTTP/3 yet, start HTTP/2 now and wait for the
+ // first that completes.
+ c2f = c2fs.get();
+ cf = CompletableFuture.anyOf(c2f, c3f);
+ }
+
+ CompletableFuture>> cfxi = cf.handle((r, t) -> {
+ if (debug.on()) {
+ debug.log("Checking which from HTTP/2 or HTTP/3 succeeded first");
+ }
+ CompletableFuture extends ExchangeImpl> res;
+ // first check if c3f is completed successfully
+ if (c3f.isDone()) {
+ Http3Connection h3c = c3f.exceptionally((e) -> null).resultNow();
+ if (h3c != null) {
+ // HTTP/3 success! Use HTTP/3
+ if (debug.on()) {
+ debug.log("HTTP/3 connect completed first, using HTTP/3");
+ }
+ res = createExchangeImpl(h3c, null, exchange, connection);
+ if (c2f != null) c2f.thenApply(c -> {
+ if (c != null) {
+ c.abandonStream();
+ }
+ return c;
+ });
+ } else {
+ // HTTP/3 failed! Use HTTP/2
+ if (debug.on()) {
+ debug.log("HTTP/3 connect completed unsuccessfully," +
+ " either with null or with exception - waiting for HTTP/2");
+ c3f.handle((r3, t3) -> {
+ debug.log("\tcf3: result=%s, throwable=%s",
+ r3, Utils.getCompletionCause(t3));
+ return r3;
+ }).exceptionally((e) -> null).join();
+ }
+ // c2f may be null here in the case where c3f was already completed
+ // when firstToComplete was called.
+ var h2cf = c2f == null ? c2fs.get() : c2f;
+ // local variable required here; see JDK-8223553
+ CompletableFuture>> fxi = h2cf
+ .handle((h2c, e) -> createExchangeImpl(h2c, e, exchange, connection));
+ res = fxi.thenCompose(x -> x);
+ }
+ } else if (c2f != null && c2f.isDone()) {
+ Http2Connection h2c = c2f.exceptionally((e) -> null).resultNow();
+ if (h2c != null) {
+ // HTTP/2 succeeded first! Use it.
+ if (debug.on()) {
+ debug.log("HTTP/2 connect completed first, using HTTP/2");
+ }
+ res = createExchangeImpl(h2c, null, exchange, connection);
+ } else if (exchange.multi.requestCancelled()) {
+ // special case for when the exchange is cancelled
+ if (debug.on()) {
+ debug.log("HTTP/2 connect completed unsuccessfully, but request cancelled");
+ }
+ CompletableFuture>> fxi = c2f
+ .handle((c, e) -> createExchangeImpl(c, e, exchange, connection));
+ res = fxi.thenCompose(x -> x);
+ } else {
+ if (debug.on()) {
+ debug.log("HTTP/2 connect completed unsuccessfully," +
+ " either with null or with exception");
+ c2f.handle((r2, t2) -> {
+ debug.log("\tcf2: result=%s, throwable=%s",
+ r2, Utils.getCompletionCause(t2));
+ return r2;
+ }).exceptionally((e) -> null).join();
+ }
+
+ // Now is the more complex stuff.
+ // HTTP/2 could have failed in the ALPN, but we still
+ // created a valid TLS connection to the server => default
+ // to HTTP/1.1 over TLS
+ HttpConnection http1Connection = null;
+ if (c2f.isCompletedExceptionally() && !c2f.isCancelled()) {
+ Throwable cause = Utils.getCompletionCause(c2f.exceptionNow());
+ if (cause instanceof ALPNException alpn) {
+ debug.log("HTTP/2 downgraded to HTTP/1.1 - use HTTP/1.1");
+ http1Connection = alpn.getConnection();
+ }
+ }
+ if (http1Connection != null) {
+ if (debug.on()) {
+ debug.log("HTTP/1.1 connect completed first, using HTTP/1.1");
+ }
+ // ALPN failed - but we have a valid HTTP/1.1 connection
+ // to the server: use that.
+ res = createHttp1Exchange(exchange, http1Connection);
+ } else {
+ if (c2f.isCompletedExceptionally()) {
+ // Wait for HTTP/3 to complete, potentially fallback to
+ // HTTP/1.1
+ // local variable required here; see JDK-8223553
+ debug.log("HTTP/2 completed with exception, wait for HTTP/3, " +
+ "possibly fallback to HTTP/1.1");
+ CompletableFuture>> fxi = c3f
+ .handle((h3c, e) -> fallbackToHttp1OnTimeout(h3c, e, exchange, connection));
+ res = fxi.thenCompose(x -> x);
+ } else {
+ //
+ // r2 == null && t2 == null - which means we know the
+ // server doesn't support h2, and we probably already
+ // have an HTTP/1.1 connection to it
+ //
+ // If an HTTP/1.1 connection is available use it.
+ // Otherwise, wait for the HTTP/3 to complete, potentially
+ // fallback to HTTP/1.1
+ HttpRequestImpl request = exchange.request();
+ InetSocketAddress proxy = Utils.resolveAddress(request.proxy());
+ InetSocketAddress addr = request.getAddress();
+ ConnectionPool pool = exchange.client().connectionPool();
+ // if we have an HTTP/1.1 connection in the pool, use that.
+ http1Connection = pool.getConnection(true, addr, proxy);
+ if (http1Connection != null && http1Connection.isOpen()) {
+ debug.log("Server doesn't support HTTP/2, " +
+ "but we have an HTTP/1.1 connection in the pool");
+ debug.log("Using HTTP/1.1");
+ res = createHttp1Exchange(exchange, http1Connection);
+ } else {
+ // we don't have anything ready to use in the pool:
+ // wait for http/3 to complete, possibly falling back
+ // to HTTP/1.1
+ debug.log("Server doesn't support HTTP/2, " +
+ "and we do not have an HTTP/1.1 connection");
+ debug.log("Waiting for HTTP/3, possibly fallback to HTTP/1.1");
+ CompletableFuture>> fxi = c3f
+ .handle((h3c, e) -> fallbackToHttp1OnTimeout(h3c, e, exchange, connection));
+ res = fxi.thenCompose(x -> x);
+ }
+ }
+ }
+ }
+ } else {
+ assert c2f != null;
+ Throwable failed = t != null ? t : new InternalError("cf1 or cf2 should have completed");
+ res = MinimalFuture.failedFuture(failed);
+ }
+ return res;
+ });
+ return cfxi.thenCompose(x -> x);
+ }
+
+ private static CompletableFuture extends ExchangeImpl>
+ fallbackToHttp1OnTimeout(Http3Connection c,
+ Throwable t,
+ Exchange exchange,
+ HttpConnection connection) {
+ if (t != null) {
+ Throwable cause = Utils.getCompletionCause(t);
+ if (cause instanceof HttpConnectTimeoutException) {
+ // when we reach here we already tried with HTTP/2,
+ // and we most likely have an HTTP/1.1 connection in
+ // the idle pool. So fallback to that.
+ if (debug.on()) {
+ debug.log("HTTP/3 connection timed out: fall back to HTTP/1.1");
+ }
+ return createHttp1Exchange(exchange, null);
+ }
+ }
+ return createExchangeImpl(c, t, exchange, connection);
+ }
+
+
+
+ // Creates an HTTP/3 exchange, possibly downgrading to HTTP/2
+ private static CompletableFuture extends ExchangeImpl>
+ createExchangeImpl(Http3Connection c,
+ Throwable t,
+ Exchange exchange,
+ HttpConnection connection) {
+ if (debug.on())
+ debug.log("handling HTTP/3 connection creation result");
+ if (t == null && exchange.multi.requestCancelled()) {
+ return MinimalFuture.failedFuture(new IOException("Request cancelled"));
+ }
+ if (c == null && t == null) {
+ if (debug.on())
+ debug.log("downgrading to HTTP/2");
+ return attemptHttp2Exchange(exchange, connection);
+ } else if (t != null) {
+ t = Utils.getCompletionCause(t);
+ if (debug.on()) {
+ if (t instanceof HttpConnectTimeoutException || t instanceof ConnectException) {
+ debug.log("HTTP/3 connection creation failed: " + t);
+ } else {
+ debug.log("HTTP/3 connection creation failed "
+ + "with unexpected exception:", t);
+ }
+ }
+ return MinimalFuture.failedFuture(t);
+ } else {
+ if (debug.on())
+ debug.log("creating HTTP/3 exchange");
+ try {
+ if (exchange.hasReachedStreamLimit()) {
+ // clear the flag before attempting to create a stream again
+ exchange.streamLimitReached(false);
+ }
+ return c.createStream(exchange)
+ .thenApply(ExchangeImpl::checkCancelled);
+ } catch (IOException e) {
+ return MinimalFuture.failedFuture(e);
+ }
+ }
+ }
+
+ private static > T checkCancelled(T exchangeImpl) {
+ Exchange> e = exchangeImpl.getExchange();
+ if (debug.on()) {
+ debug.log("checking cancellation for: " + exchangeImpl);
+ }
+ if (e.multi.requestCancelled()) {
+ if (debug.on()) {
+ debug.log("request was cancelled");
+ }
+ if (!exchangeImpl.isCanceled()) {
+ if (debug.on()) {
+ debug.log("cancelling exchange: " + exchangeImpl);
+ }
+ var cause = e.getCancelCause();
+ if (cause == null) cause = new IOException("Request cancelled");
+ exchangeImpl.cancel(cause);
+ }
+ }
+ return exchangeImpl;
+ }
+
+
+ // Creates an HTTP/2 exchange, possibly downgrading to HTTP/1
private static CompletableFuture extends ExchangeImpl>
createExchangeImpl(Http2Connection c,
Throwable t,
@@ -280,12 +676,4 @@ abstract class ExchangeImpl {
// an Expect-Continue
void expectContinueFailed(int rcode) { }
- final boolean isUnprocessedByPeer() {
- return this.unprocessedByPeer;
- }
-
- // Marks the exchange as unprocessed by the peer
- final void markUnprocessedByPeer() {
- this.unprocessedByPeer = true;
- }
}
diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/H3FrameOrderVerifier.java b/src/java.net.http/share/classes/jdk/internal/net/http/H3FrameOrderVerifier.java
new file mode 100644
index 00000000000..3eb4d631c75
--- /dev/null
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/H3FrameOrderVerifier.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (c) 2022, 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. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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.internal.net.http;
+
+import jdk.internal.net.http.http3.frames.DataFrame;
+import jdk.internal.net.http.http3.frames.HeadersFrame;
+import jdk.internal.net.http.http3.frames.Http3Frame;
+import jdk.internal.net.http.http3.frames.Http3FrameType;
+import jdk.internal.net.http.http3.frames.MalformedFrame;
+import jdk.internal.net.http.http3.frames.PushPromiseFrame;
+import jdk.internal.net.http.http3.frames.SettingsFrame;
+import jdk.internal.net.http.http3.frames.UnknownFrame;
+
+/**
+ * Verifies that when a HTTP3 frame arrives on a stream, then that particular frame type
+ * is in the expected order as compared to the previous frame type that was received.
+ * In effect, does what the RFC-9114, section 4.1 and section 6.2.1 specifies.
+ * Note that the H3FrameOrderVerifier is only responsible for checking the order in which a
+ * frame type is received on a stream. It isn't responsible for checking if that particular frame
+ * type is expected to be received on a particular stream type.
+ */
+abstract class H3FrameOrderVerifier {
+ long currentProcessingFrameType = -1; // -1 implies no frame being processed currently
+ long lastCompletedFrameType = -1; // -1 implies no frame processing has completed yet
+
+ /**
+ * {@return a frame order verifier for HTTP3 request/response stream}
+ */
+ static H3FrameOrderVerifier newForRequestResponseStream() {
+ return new ResponseStreamVerifier(false);
+ }
+
+ /**
+ * {@return a frame order verifier for HTTP3 push promise stream}
+ */
+ static H3FrameOrderVerifier newForPushPromiseStream() {
+ return new ResponseStreamVerifier(true);
+ }
+
+ /**
+ * {@return a frame order verifier for HTTP3 control stream}
+ */
+ static H3FrameOrderVerifier newForControlStream() {
+ return new ControlStreamVerifier();
+ }
+
+ /**
+ * @param frame The frame that has been received
+ * {@return true if the {@code frameType} processing can start. false otherwise}
+ */
+ abstract boolean allowsProcessing(final Http3Frame frame);
+
+ /**
+ * Marks the receipt of complete content of a frame that was currently being processed
+ *
+ * @param frame The frame whose content was fully received
+ * @throws IllegalStateException If the passed frame type wasn't being currently processed
+ */
+ void completed(final Http3Frame frame) {
+ if (frame instanceof UnknownFrame) {
+ return;
+ }
+ final long frameType = frame.type();
+ if (currentProcessingFrameType != frameType) {
+ throw new IllegalStateException("Unexpected completion of processing " +
+ "of frame type (" + frameType + "): "
+ + Http3FrameType.asString(frameType) + ", expected " +
+ Http3FrameType.asString(currentProcessingFrameType));
+ }
+ currentProcessingFrameType = -1;
+ lastCompletedFrameType = frameType;
+ }
+
+ private static final class ControlStreamVerifier extends H3FrameOrderVerifier {
+
+ @Override
+ boolean allowsProcessing(final Http3Frame frame) {
+ if (frame instanceof MalformedFrame) {
+ // a malformed frame can come in any time, so we allow it to be processed
+ // and we don't "track" it either
+ return true;
+ }
+ if (frame instanceof UnknownFrame) {
+ // unknown frames can come in any time, we allow them to be processed
+ // and we don't track their processing/completion. However, if an unknown frame
+ // is the first frame on a control stream then that's an error and we return "false"
+ // to prevent processing that frame.
+ // RFC-9114, section 9, which states - "where a known frame type is required to be
+ // in a specific location, such as the SETTINGS frame as the first frame of the
+ // control stream, an unknown frame type does not satisfy that requirement and
+ // SHOULD be treated as an error"
+ return lastCompletedFrameType != -1;
+ }
+ final long frameType = frame.type();
+ if (currentProcessingFrameType != -1) {
+ // we are in the middle of processing a particular frame type and we
+ // only expect additional frames of only that type
+ return frameType == currentProcessingFrameType;
+ }
+ // we are not currently processing any frame
+ if (lastCompletedFrameType == -1) {
+ // there was no previous frame either, so this is the first frame to have been
+ // received
+ if (frameType != SettingsFrame.TYPE) {
+ // unexpected first frame type
+ return false;
+ }
+ currentProcessingFrameType = frameType;
+ // expected first frame type
+ return true;
+ }
+ // there's no specific ordering specified on control stream other than expecting
+ // the SETTINGS frame to be the first received (which we have already verified before
+ // reaching here)
+ currentProcessingFrameType = frameType;
+ return true;
+ }
+ }
+
+ private static final class ResponseStreamVerifier extends H3FrameOrderVerifier {
+ private boolean headerSeen;
+ private boolean dataSeen;
+ private boolean trailerCompleted;
+ private final boolean pushStream;
+
+ private ResponseStreamVerifier(boolean pushStream) {
+ this.pushStream = pushStream;
+ }
+
+ @Override
+ boolean allowsProcessing(final Http3Frame frame) {
+ if (frame instanceof MalformedFrame) {
+ // a malformed frame can come in any time, so we allow it to be processed
+ // and we don't track their processing/completion
+ return true;
+ }
+ if (frame instanceof UnknownFrame) {
+ // unknown frames can come in any time, we allow them to be processed
+ // and we don't track their processing/completion
+ return true;
+ }
+ final long frameType = frame.type();
+ if (currentProcessingFrameType != -1) {
+ // we are in the middle of processing a particular frame type and we
+ // only expect additional frames of only that type
+ return frameType == currentProcessingFrameType;
+ }
+ if (frameType == DataFrame.TYPE) {
+ if (!headerSeen || trailerCompleted) {
+ // DATA is not permitted before HEADERS or after trailer
+ return false;
+ }
+ dataSeen = true;
+ } else if (frameType == HeadersFrame.TYPE) {
+ if (trailerCompleted) {
+ // HEADERS is not permitted after trailer
+ return false;
+ }
+ headerSeen = true;
+ if (dataSeen) {
+ trailerCompleted = true;
+ }
+ } else if (frameType == PushPromiseFrame.TYPE) {
+ // a push promise is only permitted on a response,
+ // and not on a push stream
+ if (pushStream) {
+ return false;
+ }
+ } else {
+ // no other frames permitted
+ return false;
+ }
+
+ currentProcessingFrameType = frameType;
+ return true;
+ }
+ }
+}
diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http1Exchange.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http1Exchange.java
index ecc4a63c9d0..02ce63b6314 100644
--- a/src/java.net.http/share/classes/jdk/internal/net/http/Http1Exchange.java
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http1Exchange.java
@@ -244,7 +244,7 @@ class Http1Exchange extends ExchangeImpl {
this.connection = connection;
} else {
InetSocketAddress addr = request.getAddress();
- this.connection = HttpConnection.getConnection(addr, client, request, HTTP_1_1);
+ this.connection = HttpConnection.getConnection(addr, client, exchange, request, HTTP_1_1);
}
this.requestAction = new Http1Request(request, this);
this.asyncReceiver = new Http1AsyncReceiver(executor, this);
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 92a48d901ff..cc8a2a7142b 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
@@ -76,7 +76,7 @@ class Http2ClientImpl {
/**
* When HTTP/2 requested only. The following describes the aggregate behavior including the
- * calling code. In all cases, the HTTP2 connection cache
+ * calling code. In all cases, the HTTP/2 connection cache
* is checked first for a suitable connection and that is returned if available.
* If not, a new connection is opened, except in https case when a previous negotiate failed.
* In that case, we want to continue using http/1.1. When a connection is to be opened and
@@ -144,6 +144,7 @@ class Http2ClientImpl {
if (conn != null) {
try {
conn.reserveStream(true, exchange.pushEnabled());
+ exchange.connectionAborter.clear(conn.connection);
} catch (IOException e) {
throw new UncheckedIOException(e); // shouldn't happen
}
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 c33cc93e7dd..63889fa6af2 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
@@ -33,6 +33,7 @@ import java.lang.invoke.VarHandle;
import java.net.InetSocketAddress;
import java.net.ProtocolException;
import java.net.http.HttpClient;
+import java.net.http.HttpClient.Version;
import java.net.http.HttpHeaders;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
@@ -70,6 +71,7 @@ import jdk.internal.net.http.common.SequentialScheduler;
import jdk.internal.net.http.common.Utils;
import jdk.internal.net.http.common.ValidatingHeadersConsumer;
import jdk.internal.net.http.common.ValidatingHeadersConsumer.Context;
+import jdk.internal.net.http.frame.AltSvcFrame;
import jdk.internal.net.http.frame.ContinuationFrame;
import jdk.internal.net.http.frame.DataFrame;
import jdk.internal.net.http.frame.ErrorFrame;
@@ -90,6 +92,7 @@ import jdk.internal.net.http.hpack.Decoder;
import jdk.internal.net.http.hpack.DecodingCallback;
import jdk.internal.net.http.hpack.Encoder;
import static java.nio.charset.StandardCharsets.UTF_8;
+import static jdk.internal.net.http.AltSvcProcessor.processAltSvcFrame;
import static jdk.internal.net.http.frame.SettingsFrame.ENABLE_PUSH;
import static jdk.internal.net.http.frame.SettingsFrame.HEADER_TABLE_SIZE;
import static jdk.internal.net.http.frame.SettingsFrame.INITIAL_CONNECTION_WINDOW_SIZE;
@@ -527,6 +530,7 @@ class Http2Connection {
AbstractAsyncSSLConnection connection = (AbstractAsyncSSLConnection)
HttpConnection.getConnection(request.getAddress(),
h2client.client(),
+ exchange,
request,
HttpClient.Version.HTTP_2);
@@ -635,6 +639,32 @@ class Http2Connection {
return true;
}
+ void abandonStream() {
+ boolean shouldClose = false;
+ stateLock.lock();
+ try {
+ long reserved = --numReservedClientStreams;
+ assert reserved >= 0;
+ if (finalStream && reserved == 0 && streams.isEmpty()) {
+ shouldClose = true;
+ }
+ } catch (Throwable t) {
+ shutdown(t); // in case the assert fires...
+ } finally {
+ stateLock.unlock();
+ }
+
+ // We should close the connection here if
+ // it's not pooled. If it's not pooled it will
+ // be marked final stream, reserved will be 0
+ // after decrementing it by one, and there should
+ // be no active request-response streams.
+ if (shouldClose) {
+ shutdown(new IOException("HTTP/2 connection abandoned"));
+ }
+
+ }
+
boolean shouldClose() {
stateLock.lock();
try {
@@ -1218,6 +1248,8 @@ class Http2Connection {
case PingFrame.TYPE -> handlePing((PingFrame) frame);
case GoAwayFrame.TYPE -> handleGoAway((GoAwayFrame) frame);
case WindowUpdateFrame.TYPE -> handleWindowUpdate((WindowUpdateFrame) frame);
+ case AltSvcFrame.TYPE -> processAltSvcFrame(0, (AltSvcFrame) frame,
+ connection, connection.client());
default -> protocolError(ErrorFrame.PROTOCOL_ERROR);
}
@@ -1323,7 +1355,8 @@ class Http2Connection {
try {
// idleConnectionTimeoutEvent is always accessed within a lock protected block
if (streams.isEmpty() && idleConnectionTimeoutEvent == null) {
- idleConnectionTimeoutEvent = client().idleConnectionTimeout()
+ final HttpClient.Version version = Version.HTTP_2;
+ idleConnectionTimeoutEvent = client().idleConnectionTimeout(version)
.map(IdleConnectionTimeoutEvent::new)
.orElse(null);
if (idleConnectionTimeoutEvent != null) {
@@ -1367,6 +1400,7 @@ class Http2Connection {
String protocolError = "protocol error" + (msg == null?"":(": " + msg));
ProtocolException protocolException =
new ProtocolException(protocolError);
+ this.cause.compareAndSet(null, protocolException);
if (markHalfClosedLocal()) {
framesDecoder.close(protocolError);
subscriber.stop(protocolException);
@@ -1844,8 +1878,16 @@ class Http2Connection {
} finally {
Throwable x = errorRef.get();
if (x != null) {
- if (debug.on()) debug.log("Stopping scheduler", x);
scheduler.stop();
+ if (client2.stopping()) {
+ if (debug.on()) {
+ debug.log("Stopping scheduler");
+ }
+ } else {
+ if (debug.on()) {
+ debug.log("Stopping scheduler", x);
+ }
+ }
Http2Connection.this.shutdown(x);
}
}
diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http3ClientImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http3ClientImpl.java
new file mode 100644
index 00000000000..05b27c3d529
--- /dev/null
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http3ClientImpl.java
@@ -0,0 +1,844 @@
+/*
+ * Copyright (c) 2020, 2025, 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. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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.internal.net.http;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.http.HttpOption.Http3DiscoveryMode;
+import java.net.http.UnsupportedProtocolVersionException;
+import java.nio.channels.ClosedChannelException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.ReentrantLock;
+
+import jdk.internal.net.http.AltServicesRegistry.AltService;
+import jdk.internal.net.http.common.ConnectionExpiredException;
+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;
+import jdk.internal.net.http.http3.Http3Error;
+import jdk.internal.net.http.quic.QuicClient;
+import jdk.internal.net.http.quic.QuicTransportParameters;
+import jdk.internal.net.quic.QuicVersion;
+import jdk.internal.net.quic.QuicTLSContext;
+
+import static java.net.http.HttpClient.Version.HTTP_3;
+import static jdk.internal.net.http.Http3ClientProperties.WAIT_FOR_PENDING_CONNECT;
+import static jdk.internal.net.http.common.Alpns.H3;
+import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.initial_max_stream_data_bidi_remote;
+import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.initial_max_streams_bidi;
+
+/**
+ * Http3 specific aspects of HttpClientImpl
+ */
+final class Http3ClientImpl implements AutoCloseable {
+ // Setting this property disables HTTPS hostname verification. Use with care.
+ private static final boolean disableHostnameVerification = Utils.isHostnameVerificationDisabled();
+ // QUIC versions in their descending order of preference
+ private static final List availableQuicVersions;
+ static {
+ // we default to QUIC v1 followed by QUIC v2, if no specific preference cannot be
+ // determined
+ final List defaultPref = List.of(QuicVersion.QUIC_V1, QuicVersion.QUIC_V2);
+ // check user specified preference
+ final String sysPropVal = Utils.getProperty("jdk.httpclient.quic.available.versions");
+ if (sysPropVal == null || sysPropVal.isBlank()) {
+ // default to supporting both v1 and v2, with v1 given preference
+ availableQuicVersions = defaultPref;
+ } else {
+ final List descendingPref = new ArrayList<>();
+ for (final String val : sysPropVal.split(",")) {
+ final QuicVersion qv;
+ try {
+ // parse QUIC version number represented as a hex string
+ final var vernum = Integer.parseInt(val.trim(), 16);
+ qv = QuicVersion.of(vernum).orElse(null);
+ } catch (NumberFormatException nfe) {
+ // ignore and continue with next
+ continue;
+ }
+ if (qv == null) {
+ continue;
+ }
+ descendingPref.add(qv);
+ }
+ availableQuicVersions = descendingPref.isEmpty() ? defaultPref : descendingPref;
+ }
+ }
+
+ private final Logger debug = Utils.getDebugLogger(this::dbgString);
+
+ final HttpClientImpl client;
+ private final Http3ConnectionPool connections = new Http3ConnectionPool(debug);
+ private final Http3PendingConnections reconnections = new Http3PendingConnections();
+ private final Set pendingClose = ConcurrentHashMap.newKeySet();
+ private final Set noH3 = ConcurrentHashMap.newKeySet();
+
+ private final QuicClient quicClient;
+ private volatile boolean closed;
+ private final AtomicReference errorRef = new AtomicReference<>();
+ private final ReentrantLock lock = new ReentrantLock();
+
+ Http3ClientImpl(HttpClientImpl client) {
+ this.client = client;
+ var executor = client.theExecutor().safeDelegate();
+ var context = client.theSSLContext();
+ var parameters = client.sslParameters();
+ if (!disableHostnameVerification) {
+ // setting the endpoint identification algo to HTTPS ensures that
+ // during the TLS handshake, the cert presented by the server is verified
+ // for hostname checks against the SNI hostname(s) set by the client
+ // or in its absence the peer's hostname.
+ // see sun.security.ssl.X509TrustManagerImpl#checkIdentity(...)
+ parameters.setEndpointIdentificationAlgorithm("HTTPS");
+ }
+ final QuicTLSContext quicTLSContext = new QuicTLSContext(context);
+ final QuicClient.Builder builder = new QuicClient.Builder();
+ builder.availableVersions(availableQuicVersions)
+ .tlsContext(quicTLSContext)
+ .sslParameters(parameters)
+ .executor(executor)
+ .applicationErrors(Http3Error::stringForCode)
+ .clientId(client.dbgString());
+ if (client.localAddress() != null) {
+ builder.bindAddress(new InetSocketAddress(client.localAddress(), 0));
+ }
+ final QuicTransportParameters transportParameters = new QuicTransportParameters();
+ // HTTP/3 doesn't allow remote bidirectional stream
+ transportParameters.setIntParameter(initial_max_streams_bidi, 0);
+ // HTTP/3 doesn't allow remote bidirectional stream: no need to allow data
+ transportParameters.setIntParameter(initial_max_stream_data_bidi_remote, 0);
+ builder.transportParameters(transportParameters);
+ this.quicClient = builder.build();
+ }
+
+ // Records an exchange waiting for a connection recovery to complete.
+ // A connection recovery happens when a connection has maxed out its number
+ // of streams, and no MAX_STREAM frame has arrived. In that case, the connection
+ // is abandoned (marked with setFinalStream() and taken out of the pool) and a
+ // new connection is initiated. Waiters are waiting for the new connection
+ // handshake to finish and for the connection to be put in the pool.
+ record Waiter(MinimalFuture cf, HttpRequestImpl request, Exchange> exchange) {
+ void complete(Http3Connection conn, Throwable error) {
+ if (error != null) cf.completeExceptionally(error);
+ else cf.complete(conn);
+ }
+ static Waiter of(HttpRequestImpl request, Exchange> exchange) {
+ return new Waiter(new MinimalFuture<>(), request, exchange);
+ }
+ }
+
+ // Indicates that recovery is needed, or in progress, for a given
+ // connection
+ sealed interface ConnectionRecovery permits PendingConnection, StreamLimitReached {
+ }
+
+ // Indicates that recovery of a connection has been initiated.
+ // Waiters will be put in wait until the handshake is completed
+ // and the connection is inserted in the pool
+ record PendingConnection(AltService altSvc, Exchange> exchange, ConcurrentLinkedQueue waiters)
+ implements ConnectionRecovery {
+ PendingConnection(AltService altSvc, Exchange> exchange, ConcurrentLinkedQueue waiters) {
+ this.altSvc = altSvc;
+ this.waiters = Objects.requireNonNull(waiters);
+ this.exchange = exchange;
+ }
+ PendingConnection(AltService altSvc, Exchange> exchange) {
+ this(altSvc, exchange, new ConcurrentLinkedQueue<>());
+ }
+ }
+
+ // Indicates that a connection that was in the pool has maxed out
+ // its stream limit and will be taken out of the pool. A new connection
+ // will be created for the first request/response exchange that needs
+ // it.
+ record StreamLimitReached(Http3Connection connection) implements ConnectionRecovery {}
+
+ // Called when recovery is needed for a given connection, with
+ // the request that got the StreamLimitException
+ public void streamLimitReached(Http3Connection connection, HttpRequestImpl request) {
+ lock.lock();
+ try {
+ reconnections.streamLimitReached(connectionKey(request), connection);
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ HttpClientImpl client() {
+ return client;
+ }
+
+ String dbgString() {
+ return "Http3ClientImpl(" + client.dbgString() + ")";
+ }
+
+ QuicClient quicClient() {
+ return this.quicClient;
+ }
+
+ String connectionKey(HttpRequestImpl request) {
+ return connections.connectionKey(request);
+ }
+
+ Http3Connection findPooledConnectionFor(HttpRequestImpl request,
+ Exchange> exchange)
+ throws IOException {
+ if (request.secure() && request.proxy() == null) {
+ final var pooled = connections.lookupFor(request);
+ if (pooled == null) {
+ return null;
+ }
+ if (pooled.tryReserveForPoolCheckout() && !pooled.isFinalStream()) {
+ final var altService = pooled.connection()
+ .getSourceAltService().orElse(null);
+ if (altService != null) {
+ // if this connection was created because it was advertised by some alt-service
+ // then verify that the alt-service is still valid/active
+ if (altService.wasAdvertised() && !client.registry().isActive(altService)) {
+ if (debug.on()) {
+ debug.log("Alt-Service %s for pooled connection has expired," +
+ " marking the connection as unusable for new streams", altService);
+ }
+ // alt-service that was the reason for this H3 connection to be created (and pooled)
+ // is no longer valid. We set a state on the connection to disallow any new streams
+ // and be auto-closed when all current streams are done
+ pooled.setFinalStreamAndCloseIfIdle();
+ return null;
+ }
+ }
+ if (debug.on()) {
+ debug.log("Found Http3Connection in connection pool");
+ }
+ // found a valid connection in pool, return it
+ return pooled;
+ } else {
+ if (debug.on()) {
+ debug.log("Pooled connection expired. Removing it.");
+ }
+ removeFromPool(pooled);
+ }
+ }
+ return null;
+ }
+
+ private static String label(Http3Connection conn) {
+ return Optional.ofNullable(conn)
+ .map(Http3Connection::connection)
+ .map(HttpQuicConnection::label)
+ .orElse("null");
+ }
+
+ private static String describe(HttpRequestImpl request, long id) {
+ return String.format("%s #%s", request, id);
+ }
+
+ private static String describe(Exchange> exchange) {
+ if (exchange == null) return "null";
+ return describe(exchange.request, exchange.multi.id);
+ }
+
+ private static String describePendingExchange(String prefix, PendingConnection pending) {
+ return String.format("%s %s", prefix, describe(pending.exchange));
+ }
+
+ private static String describeAltSvc(PendingConnection pendingConnection) {
+ return Optional.ofNullable(pendingConnection)
+ .map(PendingConnection::altSvc)
+ .map(AltService::toString)
+ .map(s -> "altsvc: " + s)
+ .orElse("no altSvc");
+ }
+
+ // Called after a recovered connection has been put back in the pool
+ // (or when recovery has failed), or when a new connection handshake
+ // has completed.
+ // Waiters, if any, will be notified.
+ private void connectionCompleted(String connectionKey, Exchange> origExchange, Http3Connection conn, Throwable error) {
+ try {
+ if (Log.http3()) {
+ Log.logHttp3("Checking waiters on completed connection {0} to {1} created for {2}",
+ label(conn), connectionKey, describe(origExchange));
+ }
+ connectionCompleted0(connectionKey, origExchange, conn, error);
+ } catch (Throwable t) {
+ if (Log.http3() || Log.errors()) {
+ Log.logError(t);
+ }
+ throw t;
+ }
+ }
+
+ private void connectionCompleted0(String connectionKey, Exchange> origExchange, Http3Connection conn, Throwable error) {
+ lock.lock();
+ // There should be a connection in the pool at this point,
+ // so we can remove the PendingConnection from the reconnections list;
+ PendingConnection pendingConnection = null;
+ try {
+ var recovery = reconnections.removeCompleted(connectionKey, origExchange, conn);
+ if (recovery instanceof PendingConnection pending) {
+ pendingConnection = pending;
+ }
+ } finally {
+ lock.unlock();
+ }
+ if (pendingConnection == null) {
+ if (Log.http3()) {
+ Log.logHttp3("No waiters to complete for " + label(conn));
+ }
+ return;
+ }
+
+ int waitersCount = pendingConnection.waiters.size();
+ if (waitersCount != 0 && Log.http3()) {
+ Log.logHttp3("Completing " + waitersCount
+ + " waiters on recreated connection " + label(conn)
+ + describePendingExchange(" - originally created for", pendingConnection));
+ }
+
+ // now for each waiter we're going to try to complete it.
+ // however, there may be more waiters than available streams!
+ // so it's rinse and repeat at this point
+ boolean origExchangeCancelled = origExchange == null ? false : origExchange.multi.requestCancelled();
+ int completedWaiters = 0;
+ int errorWaiters = 0;
+ int retriedWaiters = 0;
+ try {
+ while (!pendingConnection.waiters.isEmpty()) {
+ var waiter = pendingConnection.waiters.poll();
+ if (error != null && (!origExchangeCancelled || waiter.exchange == origExchange)) {
+ if (Log.http3()) {
+ Log.logHttp3("Completing pending waiter for: " + waiter.request + " #"
+ + waiter.exchange.multi.id + " with " + error);
+ } else if (debug.on()) {
+ debug.log("Completing waiter for: " + waiter.request
+ + " #" + waiter.exchange.multi.id + " with " + conn + " error=" + error);
+ }
+ errorWaiters++;
+ waiter.complete(conn, error);
+ } else {
+ var request = waiter.request;
+ var exchange = waiter.exchange;
+ try {
+ Http3Connection pooled = findPooledConnectionFor(request, exchange);
+ if (pooled != null && !pooled.isFinalStream() && !waiter.cf.isDone()) {
+ if (Log.http3()) {
+ Log.logHttp3("Completing pending waiter for: " + waiter.request + " #"
+ + waiter.exchange.multi.id + " with " + label(pooled));
+ } else if (debug.on()) {
+ debug.log("Completing waiter for: " + waiter.request
+ + " #" + waiter.exchange.multi.id + " with pooled conn " + label(pooled));
+ }
+ completedWaiters++;
+ waiter.cf.complete(pooled);
+ } else if (!waiter.cf.isDone()) {
+ // we call getConnectionFor: it should put waiter in the
+ // new waiting list, or attempt to open a connection again
+ if (conn != null) {
+ if (Log.http3()) {
+ Log.logHttp3("Not enough streams on recreated connection for: " + waiter.request + " #"
+ + waiter.exchange.multi.id + " with " + label(conn));
+ } else if (debug.on()) {
+ debug.log("Not enough streams on recreated connection for: " + waiter.request
+ + " #" + waiter.exchange.multi.id + " with " + label(conn)
+ + ": retrying on new connection");
+ }
+ retriedWaiters++;
+ getConnectionFor(request, exchange, waiter);
+ } else {
+ if (Log.http3()) {
+ Log.logHttp3("No HTTP/3 connection for:: " + waiter.request + " #"
+ + waiter.exchange.multi.id + ": will downgrade or fail");
+ } else if (debug.on()) {
+ debug.log("No HTTP/3 connection for: " + waiter.request
+ + " #" + waiter.exchange.multi.id + ": will downgrade or fail");
+ }
+ completedWaiters++;
+ waiter.complete(null, error);
+ }
+ }
+ } catch (Throwable t) {
+ if (debug.on()) {
+ debug.log("Completing waiter for: " + waiter.request
+ + " #" + waiter.exchange.multi.id + " with error: "
+ + Utils.getCompletionCause(t));
+ }
+ var cause = Utils.getCompletionCause(t);
+ if (cause instanceof ClosedChannelException) {
+ cause = new ConnectionExpiredException(cause);
+ }
+ if (Log.http3()) {
+ Log.logHttp3("Completing pending waiter for: " + waiter.request + " #"
+ + waiter.exchange.multi.id + " with " + cause);
+ }
+ errorWaiters++;
+ waiter.cf.completeExceptionally(cause);
+ }
+ }
+ }
+ } finally {
+ if (Log.http3()) {
+ String pendingInfo = describePendingExchange(" - originally created for", pendingConnection);
+
+ if (conn != null) {
+ Log.logHttp3(("Connection creation completed for requests to %s: " +
+ "waiters[%s](completed:%s, retried:%s, errors:%s)%s")
+ .formatted(connectionKey, waitersCount, completedWaiters,
+ retriedWaiters, errorWaiters, pendingInfo));
+ } else {
+ Log.logHttp3(("No HTTP/3 connection created for requests to %s, will fail or downgrade: " +
+ "waiters[%s](completed:%s, retried:%s, errors:%s)%s")
+ .formatted(connectionKey, waitersCount, completedWaiters,
+ retriedWaiters, errorWaiters, pendingInfo));
+ }
+ }
+ }
+ }
+
+ CompletableFuture getConnectionFor(HttpRequestImpl request, Exchange> exchange) {
+ assert request != null;
+ return getConnectionFor(request, exchange, null);
+ }
+
+ private void completeWaiter(Logger debug, Waiter pendingWaiter, Http3Connection r, Throwable t) {
+ // the recovery was done on behalf of a pending waiter.
+ // this can happen if the new connection has already maxed out,
+ // and recovery was initiated on behalf of the next waiter.
+ if (Log.http3()) {
+ Log.logHttp3("Completing waiter for: " + pendingWaiter.request + " #"
+ + pendingWaiter.exchange.multi.id + " with (conn: " + label(r) + " error: " + t +")");
+ } else if (debug.on()) {
+ debug.log("Completing pending waiter for " + pendingWaiter.request + " #"
+ + pendingWaiter.exchange.multi.id + " with (conn: " + label(r) + " error: " + t +")");
+ }
+ pendingWaiter.complete(r, t);
+ }
+
+ private CompletableFuture wrapForDebug(CompletableFuture h3Cf,
+ Exchange> exchange,
+ HttpRequestImpl request) {
+ if (debug.on() || Log.http3()) {
+ if (Log.http3()) {
+ Log.logHttp3("Recreating connection for: " + request + " #"
+ + exchange.multi.id);
+ } else if (debug.on()) {
+ debug.log("Recreating connection for: " + request + " #"
+ + exchange.multi.id);
+ }
+ return h3Cf.whenComplete((r, t) -> {
+ if (Log.http3()) {
+ if (r != null && t == null) {
+ Log.logHttp3("Connection recreated for " + request + " #"
+ + exchange.multi.id + " on " + label(r));
+ } else if (t != null) {
+ Log.logHttp3("Connection creation failed for " + request + " #"
+ + exchange.multi.id + ": " + t);
+ } else if (r == null) {
+ Log.logHttp3("No connection found for " + request + " #"
+ + exchange.multi.id);
+ }
+ } else if (debug.on()) {
+ debug.log("Connection recreated for " + request + " #"
+ + exchange.multi.id);
+ }
+ });
+ } else {
+ return h3Cf;
+ }
+ }
+
+ Optional lookupAltSvc(HttpRequestImpl request) {
+ return client.registry()
+ .lookup(request.uri(), H3::equals)
+ .findFirst();
+ }
+
+ CompletableFuture getConnectionFor(HttpRequestImpl request,
+ Exchange> exchange,
+ Waiter pendingWaiter) {
+ assert request != null;
+ if (Log.http3()) {
+ if (pendingWaiter != null) {
+ Log.logHttp3("getConnectionFor pendingWaiter {0}",
+ describe(pendingWaiter.request, pendingWaiter.exchange.multi.id));
+ } else {
+ Log.logHttp3("getConnectionFor exchange {0}",
+ describe(request, exchange.multi.id));
+ }
+ }
+ try {
+ Http3Connection pooled = findPooledConnectionFor(request, exchange);
+ if (pooled != null) {
+ if (pendingWaiter != null) {
+ if (Log.http3()) {
+ Log.logHttp3("Completing pending waiter for: " + request + " #"
+ + exchange.multi.id + " with " + pooled.dbgTag());
+ } else if (debug.on()) {
+ debug.log("Completing pending waiter for: " + request + " #"
+ + exchange.multi.id + " with " + pooled.dbgTag());
+ }
+ pendingWaiter.cf.complete(pooled);
+ return pendingWaiter.cf;
+ } else {
+ return MinimalFuture.completedFuture(pooled);
+ }
+ }
+ if (request.secure() && request.proxy() == null) {
+ boolean reconnecting, waitForPendingConnect;
+ PendingConnection pendingConnection = null;
+ String key;
+ Waiter waiter = null;
+ if (reconnecting = exchange.hasReachedStreamLimit()) {
+ if (debug.on()) {
+ debug.log("Exchange has reached limit for: " + request + " #"
+ + exchange.multi.id);
+ }
+ }
+ if (pendingWaiter != null) reconnecting = true;
+ lock.lock();
+ try {
+ key = connectionKey(request);
+
+ var recovery = reconnections.lookupFor(key, request, client);
+ if (debug.on()) debug.log("lookup found %s for %s", recovery, request);
+ if (recovery instanceof PendingConnection pending) {
+ // Recovery already initiated. Add waiter to the list!
+ if (debug.on()) {
+ debug.log("PendingConnection (%s) found for %s",
+ describePendingExchange("originally created for", pending),
+ describe(request, exchange.multi.id));
+ }
+ pendingConnection = pending;
+ waiter = pendingWaiter == null
+ ? Waiter.of(request, exchange)
+ : pendingWaiter;
+ exchange.streamLimitReached(false);
+ pendingConnection.waiters.add(waiter);
+ return waiter.cf;
+ } else if (recovery instanceof StreamLimitReached) {
+ // A connection to this server has maxed out its allocated
+ // streams and will be taken out of the pool, but recovery
+ // has not been initiated yet. Do that now.
+ reconnecting = waitForPendingConnect = true;
+ } else waitForPendingConnect = WAIT_FOR_PENDING_CONNECT;
+ // By default, we allow concurrent attempts to
+ // create HTTP/3 connections to the same host, except when
+ // one connection has reached the maximum number of streams
+ // it is allowed to use. However,
+ // if waitForPendingConnect is set to `true` above we will
+ // only allow one connection to attempt handshake at a given
+ // time, other requests will be added to a pending list so
+ // that they can go through that connection.
+ if (waitForPendingConnect) {
+ // check again
+ if ((pooled = findPooledConnectionFor(request, exchange)) == null) {
+ // initiate recovery
+ var altSvc = lookupAltSvc(request).orElse(null);
+ // maybe null if ALT_SVC && altSvc == null
+ pendingConnection = reconnections.addPending(key, request, altSvc, exchange);
+ } else if (pendingWaiter != null) {
+ if (Log.http3()) {
+ Log.logHttp3("Completing pending waiter for: " + request + " #"
+ + exchange.multi.id + " with " + pooled.dbgTag());
+ } else if (debug.on()) {
+ debug.log("Completing pending waiter for: " + request + " #"
+ + exchange.multi.id + " with " + pooled.dbgTag());
+ }
+ pendingWaiter.cf.complete(pooled);
+ return pendingWaiter.cf;
+ } else {
+ return MinimalFuture.completedFuture(pooled);
+ }
+ }
+ } finally {
+ lock.unlock();
+ if (waiter != null && waiter != pendingWaiter && Log.http3()) {
+ var altSvc = describeAltSvc(pendingConnection);
+ var orig = Optional.of(pendingConnection)
+ .map(PendingConnection::exchange)
+ .map(e -> " created for #" + e.multi.id)
+ .orElse("");
+ Log.logHttp3("Waiting for connection for: " + describe(request, exchange.multi.id)
+ + " " + altSvc + orig);
+ } else if (pendingWaiter != null && Log.http3()) {
+ var altSvc = describeAltSvc(pendingConnection);
+ Log.logHttp3("Creating connection for: " + describe(request, exchange.multi.id)
+ + " " + altSvc);
+ } else if (debug.on() && waiter != null) {
+ debug.log("Waiting for connection for: " + describe(request, exchange.multi.id)
+ + (waiter == pendingWaiter ? " (still pending)" : ""));
+ }
+ }
+
+ if (Log.http3()) {
+ Log.logHttp3("Creating connection for Exchange {0}", describe(exchange));
+ } else if (debug.on()) {
+ debug.log("Creating connection for Exchange %s", describe(exchange));
+ }
+
+ CompletableFuture h3Cf = Http3Connection
+ .createAsync(request, this, exchange);
+ if (reconnecting) {
+ // System.err.println("Recreating connection for: " + request + " #"
+ // + exchange.multi.id);
+ h3Cf = wrapForDebug(h3Cf, exchange, request);
+ }
+ if (pendingWaiter != null) {
+ // the connection was done on behalf of a pending waiter.
+ // this can happen if the new connection has already maxed out,
+ // and recovery was initiated on behalf of the next waiter.
+ h3Cf = h3Cf.whenComplete((r,t) -> completeWaiter(debug, pendingWaiter, r, t));
+ }
+ h3Cf = h3Cf.thenApply(conn -> {
+ if (conn != null) {
+ if (debug.on()) {
+ debug.log("Offering connection %s created for %s",
+ label(conn), exchange.multi.id);
+ }
+ var offered = offerConnection(conn);
+ if (debug.on()) {
+ debug.log("Connection offered %s created for %s",
+ label(conn), exchange.multi.id);
+ }
+ // if we return null here, we will downgrade
+ // but if we return `conn` we will open a new connection.
+ return offered == null ? conn : offered;
+ } else {
+ if (debug.on()) {
+ debug.log("No connection for exchange #" + exchange.multi.id);
+ }
+ return null;
+ }
+ });
+ if (pendingConnection != null) {
+ // need to wake up waiters after successful handshake and recovery
+ h3Cf = h3Cf.whenComplete((r, t) -> connectionCompleted(key, exchange, r, t));
+ }
+ return h3Cf;
+ } else {
+ if (debug.on())
+ debug.log("Request is unsecure, or proxy isn't null: can't use HTTP/3");
+ if (request.isHttp3Only(exchange.version())) {
+ return MinimalFuture.failedFuture(new UnsupportedProtocolVersionException(
+ "can't use HTTP/3 with proxied or unsecured connection"));
+ }
+ return MinimalFuture.completedFuture(null);
+ }
+ } catch (Throwable t) {
+ if (Log.http3() || Log.errors()) {
+ Log.logError("Failed to get connection for {0}: {1}",
+ describe(exchange), t);
+ }
+ return MinimalFuture.failedFuture(t);
+ }
+ }
+
+ /*
+ * Cache the given connection, if no connection to the same
+ * destination exists. If one exists, then we let the initial stream
+ * complete but allow it to close itself upon completion.
+ * This situation should not arise with https because the request
+ * has not been sent as part of the initial alpn negotiation
+ */
+ Http3Connection offerConnection(Http3Connection c) {
+ if (debug.on()) debug.log("offering to the connection pool: %s", c);
+ if (!c.isOpen() || c.isFinalStream()) {
+ if (debug.on())
+ debug.log("skipping offered closed or closing connection: %s", c);
+ return null;
+ }
+
+ String key = c.key();
+ lock.lock();
+ try {
+ if (closed) {
+ var error = errorRef.get();
+ if (error == null) error = new IOException("client closed");
+ c.connectionError(error, Http3Error.H3_INTERNAL_ERROR);
+ return null;
+ }
+ Http3Connection c1 = connections.putIfAbsent(key, c);
+ if (c1 != null) {
+ // there was a connection in the pool
+ if (!c1.isFinalStream() || c.isFinalStream()) {
+ if (!c.isFinalStream()) {
+ c.allowOnlyOneStream();
+ return c;
+ } else if (c1.isFinalStream()) {
+ return c;
+ }
+ if (debug.on())
+ debug.log("existing entry %s in connection pool for %s", c1, key);
+ // c1 will remain in the pool and we will use c for the given
+ // request.
+ if (Log.http3()) {
+ Log.logHttp3("Existing connection {0} for {1} found in the pool", label(c1), c1.key());
+ Log.logHttp3("New connection {0} marked final and not offered to the pool", label(c));
+ }
+ return c1;
+ }
+ connections.put(key, c);
+ }
+ if (debug.on())
+ debug.log("put in the connection pool: %s", c);
+ return c;
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ void removeFromPool(Http3Connection c) {
+ lock.lock();
+ try {
+ if (connections.remove(c.key(), c)) {
+ if (debug.on())
+ debug.log("removed from the connection pool: %s", c);
+ }
+ if (c.isOpen()) {
+ if (debug.on())
+ debug.log("adding to pending close: %s", c);
+ pendingClose.add(c);
+ }
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ void connectionClosed(Http3Connection c) {
+ removeFromPool(c);
+ if (pendingClose.remove(c)) {
+ if (debug.on())
+ debug.log("removed from pending close: %s", c);
+ }
+ }
+
+ public Logger debug() { return debug;}
+
+ @Override
+ public void close() {
+ try {
+ lock.lock();
+ try {
+ closed = true;
+ pendingClose.clear();
+ connections.clear();
+ } finally {
+ lock.unlock();
+ }
+ // The client itself is being closed, so we don't individually close the connections
+ // here and instead just close the QuicClient which then initiates the close of
+ // the QUIC endpoint. That will silently terminate the underlying QUIC connections
+ // without exchanging any datagram packets with the peer, since there's no point
+ // sending/receiving those (including GOAWAY frame) when the endpoint (socket channel)
+ // itself won't be around after this point.
+ } finally {
+ quicClient.close();
+ }
+ }
+
+ // Called in case of RejectedExecutionException, or shutdownNow;
+ public void abort(Throwable t) {
+ if (debug.on()) {
+ debug.log("HTTP/3 client aborting due to " + t);
+ }
+ try {
+ errorRef.compareAndSet(null, t);
+ List connectionList;
+ lock.lock();
+ try {
+ closed = true;
+ connectionList = new ArrayList<>(connections.values().toList());
+ connectionList.addAll(pendingClose);
+ pendingClose.clear();
+ connections.clear();
+ } finally {
+ lock.unlock();
+ }
+ for (var conn : connectionList) {
+ conn.close(t);
+ }
+ } finally {
+ quicClient.abort(t);
+ }
+ }
+
+ public void stop() {
+ close();
+ }
+
+ /**
+ * After an unsuccessful H3 direct connection attempt,
+ * mark the authority as not supporting h3.
+ * @param rawAuthority the raw authority (host:port)
+ */
+ public void noH3(String rawAuthority) {
+ noH3.add(rawAuthority);
+ }
+
+ /**
+ * Tells whether the given authority has been marked as
+ * not supporting h3
+ * @param rawAuthority the raw authority (host:port)
+ * @return true if the given authority is believed to not support h3
+ */
+ public boolean hasNoH3(String rawAuthority) {
+ return noH3.contains(rawAuthority);
+ }
+
+ /**
+ * A direct HTTP/3 attempt may be attempted if we don't have an
+ * AltService h3 endpoint recorded for it, and if the given request
+ * URI's raw authority hasn't been marked as not supporting HTTP/3,
+ * and if the request discovery config is not ALT_SVC.
+ * Note that a URI may be marked has not supporting H3 if it doesn't
+ * acknowledge the first initial quic packet in the time defined
+ * by {@systemProperty jdk.httpclient.http3.maxDirectConnectionTimeout}.
+ * @param request the request that may go through h3
+ * @return true if there's no h3 endpoint already registered for the given uri.
+ */
+ public boolean mayAttemptDirectConnection(HttpRequestImpl request) {
+ var config = request.http3Discovery();
+ return switch (config) {
+ // never attempt direct connection with ALT_SVC
+ case Http3DiscoveryMode.ALT_SVC -> false;
+ // always attempt direct connection with HTTP_3_ONLY, unless
+ // it was attempted before and failed
+ case Http3DiscoveryMode.HTTP_3_URI_ONLY ->
+ !hasNoH3(request.uri().getRawAuthority());
+ // otherwise, attempt direct connection only if we have no
+ // alt service and it wasn't attempted and failed before
+ default -> lookupAltSvc(request).isEmpty()
+ && !hasNoH3(request.uri().getRawAuthority());
+ };
+ }
+}
diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http3ClientProperties.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http3ClientProperties.java
new file mode 100644
index 00000000000..81f8c8109d5
--- /dev/null
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http3ClientProperties.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright (c) 2023, 2025, 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. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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.internal.net.http;
+
+import jdk.internal.net.http.common.Utils;
+
+import static jdk.internal.net.http.http3.frames.SettingsFrame.DEFAULT_SETTINGS_MAX_FIELD_SECTION_SIZE;
+import static jdk.internal.net.http.http3.frames.SettingsFrame.DEFAULT_SETTINGS_QPACK_BLOCKED_STREAMS;
+import static jdk.internal.net.http.http3.frames.SettingsFrame.DEFAULT_SETTINGS_QPACK_MAX_TABLE_CAPACITY;
+
+/**
+ * A class that groups initial values for HTTP/3 client properties.
+ *
+ * Properties starting with {@code jdk.internal.} are not exposed and
+ * typically reserved for testing. They could be removed, and their name,
+ * semantics, or values, could be changed at any time.
+ *
+ * Properties that are exposed are JDK specifics and typically documented
+ * in the {@link java.net.http} module API documentation.
+ *
+ * -
+ *
-
+ *
+ *
+ * @apiNote
+ * Not all properties are exposed. Properties that are not included in
+ * the {@link java.net.http} module API documentation are subject to
+ * change, and should be considered internal, though we might also consider
+ * exposing them in the future if needed.
+ *
+ */
+public final class Http3ClientProperties {
+
+ private Http3ClientProperties() {
+ throw new InternalError("should not come here");
+ }
+
+ // The maximum timeout to wait for a reply to the first INITIAL
+ // packet when attempting a direct connection
+ public static final long MAX_DIRECT_CONNECTION_TIMEOUT;
+
+ // The maximum timeout to wait for a MAX_STREAM frame
+ // before throwing StreamLimitException
+ public static final long MAX_STREAM_LIMIT_WAIT_TIMEOUT;
+
+ // The maximum number of concurrent push streams
+ // by connection
+ public static final long MAX_HTTP3_PUSH_STREAMS;
+
+ // Limit for dynamic table capacity that the encoder is allowed
+ // to set. Its capacity is also limited by the QPACK_MAX_TABLE_CAPACITY
+ // HTTP/3 setting value received from the peer decoder.
+ public static final long QPACK_ENCODER_TABLE_CAPACITY_LIMIT;
+
+ // The value of SETTINGS_QPACK_MAX_TABLE_CAPACITY HTTP/3 setting that is
+ // negotiated by HTTP client's decoder
+ public static final long QPACK_DECODER_MAX_TABLE_CAPACITY;
+
+ // The value of SETTINGS_MAX_FIELD_SECTION_SIZE HTTP/3 setting that is
+ // negotiated by HTTP client's decoder
+ public static final long QPACK_DECODER_MAX_FIELD_SECTION_SIZE;
+
+ // Decoder upper bound on the number of streams that can be blocked
+ public static final long QPACK_DECODER_BLOCKED_STREAMS;
+
+ // of available space in the dynamic table
+
+ // Percentage of occupied space in the dynamic table that controls when
+ // the draining index starts increasing. This index determines which entries
+ // are too close to eviction, and can be referenced by the encoder.
+ public static final int QPACK_ENCODER_DRAINING_THRESHOLD;
+
+ // If set to "true" allows the encoder to insert a header with a dynamic
+ // name reference and reference it in a field line section without awaiting
+ // decoder's acknowledgement.
+ public static final boolean QPACK_ALLOW_BLOCKING_ENCODING = Utils.getBooleanProperty(
+ "jdk.internal.httpclient.qpack.allowBlockingEncoding", false);
+
+ // whether localhost is acceptable as an alternative service origin
+ public static final boolean ALTSVC_ALLOW_LOCAL_HOST_ORIGIN = Utils.getBooleanProperty(
+ "jdk.httpclient.altsvc.allowLocalHostOrigin", true);
+
+ // whether concurrent HTTP/3 requests to the same host should wait for
+ // first connection to succeed (or fail) instead of attempting concurrent
+ // connections. Where concurrent connections are attempted, only one of
+ // them will be offered to the connection pool. The others will serve a
+ // single request.
+ public static final boolean WAIT_FOR_PENDING_CONNECT = Utils.getBooleanProperty(
+ "jdk.httpclient.http3.waitForPendingConnect", true);
+
+
+ static {
+ // 375 is ~ to the initial loss timer
+ // 1000 is ~ the initial PTO
+ // We will set a timeout of 2*1375 ms to wait for the reply to our
+ // first initial packet for a direct connection
+ long defaultMaxDirectConnectionTimeout = 1375 << 1; // ms
+ long maxDirectConnectionTimeout = Utils.getLongProperty(
+ "jdk.httpclient.http3.maxDirectConnectionTimeout",
+ defaultMaxDirectConnectionTimeout);
+ long maxStreamLimitTimeout = Utils.getLongProperty(
+ "jdk.httpclient.http3.maxStreamLimitTimeout",
+ defaultMaxDirectConnectionTimeout);
+ int defaultMaxHttp3PushStreams = Utils.getIntegerProperty(
+ "jdk.httpclient.maxstreams",
+ 100);
+ int maxHttp3PushStreams = Utils.getIntegerProperty(
+ "jdk.httpclient.http3.maxConcurrentPushStreams",
+ defaultMaxHttp3PushStreams);
+ long defaultDecoderMaxCapacity = 0;
+ long decoderMaxTableCapacity = Utils.getLongProperty(
+ "jdk.httpclient.qpack.decoderMaxTableCapacity",
+ defaultDecoderMaxCapacity);
+ long decoderBlockedStreams = Utils.getLongProperty(
+ "jdk.httpclient.qpack.decoderBlockedStreams",
+ DEFAULT_SETTINGS_QPACK_BLOCKED_STREAMS);
+ long defaultEncoderTableCapacityLimit = 4096;
+ long encoderTableCapacityLimit = Utils.getLongProperty(
+ "jdk.httpclient.qpack.encoderTableCapacityLimit",
+ defaultEncoderTableCapacityLimit);
+ int defaultDecoderMaxFieldSectionSize = 393216; // 384kB
+ long decoderMaxFieldSectionSize = Utils.getIntegerNetProperty(
+ "jdk.http.maxHeaderSize", Integer.MIN_VALUE, Integer.MAX_VALUE,
+ defaultDecoderMaxFieldSectionSize, true);
+ // Percentage of occupied space in the dynamic table that when
+ // exceeded the dynamic table draining index starts increasing
+ int drainingThreshold = Utils.getIntegerProperty(
+ "jdk.internal.httpclient.qpack.encoderDrainingThreshold",
+ 75);
+
+ MAX_DIRECT_CONNECTION_TIMEOUT = maxDirectConnectionTimeout <= 0
+ ? defaultMaxDirectConnectionTimeout : maxDirectConnectionTimeout;
+ MAX_STREAM_LIMIT_WAIT_TIMEOUT = maxStreamLimitTimeout < 0
+ ? defaultMaxDirectConnectionTimeout
+ : maxStreamLimitTimeout;
+ MAX_HTTP3_PUSH_STREAMS = Math.max(maxHttp3PushStreams, 0);
+ QPACK_ENCODER_TABLE_CAPACITY_LIMIT = encoderTableCapacityLimit < 0
+ ? defaultEncoderTableCapacityLimit : encoderTableCapacityLimit;
+ QPACK_DECODER_MAX_TABLE_CAPACITY = decoderMaxTableCapacity < 0 ?
+ DEFAULT_SETTINGS_QPACK_MAX_TABLE_CAPACITY : decoderMaxTableCapacity;
+ QPACK_DECODER_MAX_FIELD_SECTION_SIZE = decoderMaxFieldSectionSize < 0 ?
+ DEFAULT_SETTINGS_MAX_FIELD_SECTION_SIZE : decoderMaxFieldSectionSize;
+ QPACK_DECODER_BLOCKED_STREAMS = decoderBlockedStreams < 0 ?
+ DEFAULT_SETTINGS_QPACK_BLOCKED_STREAMS : decoderBlockedStreams;
+ QPACK_ENCODER_DRAINING_THRESHOLD = Math.clamp(drainingThreshold, 10, 90);
+ }
+
+}
diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http3Connection.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http3Connection.java
new file mode 100644
index 00000000000..b97a441881d
--- /dev/null
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http3Connection.java
@@ -0,0 +1,1657 @@
+/*
+ * Copyright (c) 2020, 2025, 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. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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.internal.net.http;
+
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.VarHandle;
+import java.net.ProtocolException;
+import java.net.http.HttpHeaders;
+import java.net.http.HttpResponse.PushPromiseHandler.PushId;
+import java.net.http.HttpResponse.PushPromiseHandler.PushId.Http3PushId;
+import java.net.http.StreamLimitException;
+import java.net.http.UnsupportedProtocolVersionException;
+import java.nio.ByteBuffer;
+import java.nio.channels.ClosedChannelException;
+import java.time.Duration;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import jdk.internal.net.http.Http3PushManager.CancelPushReason;
+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;
+import jdk.internal.net.http.http3.ConnectionSettings;
+import jdk.internal.net.http.http3.Http3Error;
+import jdk.internal.net.http.http3.frames.CancelPushFrame;
+import jdk.internal.net.http.http3.frames.FramesDecoder;
+import jdk.internal.net.http.http3.frames.GoAwayFrame;
+import jdk.internal.net.http.http3.frames.Http3Frame;
+import jdk.internal.net.http.http3.frames.Http3FrameType;
+import jdk.internal.net.http.http3.frames.MalformedFrame;
+import jdk.internal.net.http.http3.frames.MaxPushIdFrame;
+import jdk.internal.net.http.http3.frames.PartialFrame;
+import jdk.internal.net.http.http3.frames.SettingsFrame;
+import jdk.internal.net.http.http3.streams.Http3Streams;
+import jdk.internal.net.http.http3.streams.Http3Streams.StreamType;
+import jdk.internal.net.http.http3.streams.PeerUniStreamDispatcher;
+import jdk.internal.net.http.http3.streams.QueuingStreamPair;
+import jdk.internal.net.http.http3.streams.UniStreamPair;
+import jdk.internal.net.http.qpack.Decoder;
+import jdk.internal.net.http.qpack.Encoder;
+import jdk.internal.net.http.qpack.QPACK;
+import jdk.internal.net.http.qpack.TableEntry;
+import jdk.internal.net.http.quic.QuicConnection;
+import jdk.internal.net.http.quic.QuicStreamLimitException;
+import jdk.internal.net.http.quic.TerminationCause;
+import jdk.internal.net.http.quic.VariableLengthEncoder;
+import jdk.internal.net.http.quic.streams.QuicBidiStream;
+import jdk.internal.net.http.quic.streams.QuicReceiverStream;
+import jdk.internal.net.http.quic.streams.QuicStream;
+import jdk.internal.net.http.quic.streams.QuicStreamWriter;
+import jdk.internal.net.http.quic.streams.QuicStreams;
+import static java.net.http.HttpClient.Version.HTTP_3;
+import static jdk.internal.net.http.Http3ClientProperties.MAX_STREAM_LIMIT_WAIT_TIMEOUT;
+import static jdk.internal.net.http.http3.Http3Error.H3_CLOSED_CRITICAL_STREAM;
+import static jdk.internal.net.http.http3.Http3Error.H3_INTERNAL_ERROR;
+import static jdk.internal.net.http.http3.Http3Error.H3_NO_ERROR;
+import static jdk.internal.net.http.http3.Http3Error.H3_STREAM_CREATION_ERROR;
+
+/**
+ * An HTTP/3 connection wraps an HttpQuicConnection and implements
+ * HTTP/3 on top it.
+ */
+public final class Http3Connection implements AutoCloseable {
+
+ private final Logger debug = Utils.getDebugLogger(this::dbgTag);
+ private final Http3ClientImpl client;
+ private final HttpQuicConnection connection;
+ private final QuicConnection quicConnection;
+ // key by which this connection will be referred to within the connection pool
+ private final String connectionKey;
+ private final String dbgTag;
+ private final UniStreamPair controlStreamPair;
+ private final UniStreamPair qpackEncoderStreams;
+ private final UniStreamPair qpackDecoderStreams;
+ private final Encoder qpackEncoder;
+ private final Decoder qpackDecoder;
+ private final FramesDecoder controlFramesDecoder;
+ private final Predicate super QuicReceiverStream> remoteStreamListener;
+ private final H3FrameOrderVerifier frameOrderVerifier = H3FrameOrderVerifier.newForControlStream();
+ // streams for HTTP3 exchanges
+ private final ConcurrentMap exchangeStreams = new ConcurrentHashMap<>();
+ private final ConcurrentMap> exchanges = new ConcurrentHashMap<>();
+ // true when the settings frame has been received on the control stream of this connection
+ private volatile boolean settingsFrameReceived;
+ // the settings we received from the peer
+ private volatile ConnectionSettings peerSettings;
+ // the settings we send to our peer
+ private volatile ConnectionSettings ourSettings;
+ // for tests
+ private final MinimalFuture peerSettingsCF = new MinimalFuture<>();
+ // the (lowest) request stream id received in GOAWAY frames on this connection.
+ // subsequent request stream id(s) (if any) must always be equal to lesser than this value
+ // as per spec
+ // -1 is used to imply no GOAWAY received so far
+ private final AtomicLong lowestGoAwayReceipt = new AtomicLong(-1);
+ private volatile IdleConnectionTimeoutEvent idleConnectionTimeoutEvent;
+ // value of true implies no more streams will be initiated on this connection,
+ // and the connection will be closed once the in-progress streams complete.
+ private volatile boolean finalStream;
+ private volatile boolean allowOnlyOneStream;
+ // set to true if we decide to open a new connection
+ // due to stream limit reached
+ private volatile boolean streamLimitReached;
+
+ private static final int GOAWAY_SENT = 1; // local endpoint sent GOAWAY
+ private static final int GOAWAY_RECEIVED = 2; // received GOAWAY from remote peer
+ private static final int CLOSED = 4; // close called on QUIC connection
+ volatile int closedState;
+
+ private final ReentrantLock lock = new ReentrantLock();
+ private final Http3PushManager pushManager;
+ private final AtomicLong reservedStreamCount = new AtomicLong();
+
+ // The largest pushId for a remote created stream.
+ // After GOAWAY has been sent, we will not accept
+ // any larger pushId.
+ private final AtomicLong largestPushId = new AtomicLong();
+
+ // The max pushId for which a frame was scheduled to be sent.
+ // This should always be less or equal to pushManager.maxPushId
+ private final AtomicLong maxPushIdSent = new AtomicLong();
+
+
+ /**
+ * Creates a new HTTP/3 connection over a given {@link HttpQuicConnection}.
+ *
+ * @apiNote
+ * This constructor is invoked upon a successful quic connection establishment,
+ * typically after a successful Quic handshake. Creating the Http3Connection
+ * earlier, for instance, after receiving the Server Hello, could also be considered.
+ *
+ * @implNote
+ * Creating an HTTP/3 connection will trigger the creation of the HTTP/3 control
+ * stream, sending of the HTTP/3 Settings frame, and creation of the QPack
+ * encoder/decoder streams.
+ *
+ * @param request the request which triggered the creation of the connection
+ * @param client the Http3Client instance this connection belongs to
+ * @param connection the {@code HttpQuicConnection} that was established
+ */
+ Http3Connection(HttpRequestImpl request, Http3ClientImpl client, HttpQuicConnection connection) {
+ this.connectionKey = client.connectionKey(request);
+ this.client = client;
+ this.connection = connection;
+ this.quicConnection = connection.quicConnection();
+ var qdb = quicConnection.dbgTag();
+ this.dbgTag = "H3(" + qdb +")";
+ this.pushManager = new Http3PushManager(this); // OK to leak this
+ controlFramesDecoder = new FramesDecoder("H3-control("+qdb+")",
+ FramesDecoder::isAllowedOnControlStream);
+ controlStreamPair = new UniStreamPair(
+ StreamType.CONTROL,
+ quicConnection,
+ this::processPeerControlBytes,
+ this::lcsWriterLoop,
+ this::controlStreamFailed,
+ debug);
+
+ qpackEncoder = new Encoder(Http3Connection::shouldUpdateDynamicTable,
+ this::createEncoderStreams, this::connectionError);
+ qpackEncoderStreams = qpackEncoder.encoderStreams();
+ qpackDecoder = new Decoder(this::createDecoderStreams, this::connectionError);
+ qpackDecoderStreams = qpackDecoder.decoderStreams();
+ // Register listener to be called when the peer opens a new stream
+ remoteStreamListener = this::onOpenRemoteStream;
+ quicConnection.addRemoteStreamListener(remoteStreamListener);
+
+ // Registers dependent actions with the controlStreamPair
+ // .futureSenderStreamWriter() CF, in order to send
+ // the SETTINGS and MAX_PUSHID frames.
+ // These actions will be executed when the stream writer is
+ // available.
+ //
+ // This will schedule the SETTINGS and MAX_PUSHID frames
+ // for writing, buffering them if necessary until control
+ // flow credits are available.
+ //
+ // If an exception happens the connection will be
+ // closed abruptly (by closing the underlying quic connection)
+ // with an error of type Http3Error.H3_INTERNAL_ERROR
+ controlStreamPair.futureSenderStreamWriter()
+ // Send SETTINGS first
+ .thenApply(this::sendSettings)
+ // Chains to sending MAX_PUSHID after SETTINGS
+ .thenApply(this::sendMaxPushId)
+ // arranges for the connection to be closed
+ // in case of exception. Throws in the dependent
+ // action after wrapping the exception if needed.
+ .exceptionally(this::exceptionallyAndClose);
+ if (Log.http3()) {
+ Log.logHttp3("HTTP/3 connection created for " + quicConnectionTag() + " - local address: "
+ + quicConnection.localAddress());
+ }
+ }
+
+ public String quicConnectionTag() {
+ return quicConnection.logTag();
+ }
+
+ private static boolean shouldUpdateDynamicTable(TableEntry tableEntry) {
+ if (tableEntry.type() == TableEntry.EntryType.NAME_VALUE) {
+ return false;
+ }
+ return switch (tableEntry.name().toString()) {
+ case ":authority", "user-agent" -> !tableEntry.value().isEmpty();
+ default -> false;
+ };
+ }
+
+ private void lock() {
+ lock.lock();
+ }
+
+ private void unlock() {
+ lock.unlock();
+ }
+
+ /**
+ * Debug tag used to create the debug logger for this
+ * HTTP/3 connection instance.
+ *
+ * @return a debug tag
+ */
+ String dbgTag() {
+ return dbgTag;
+ }
+
+ /**
+ * Asynchronously create an instance of an HTTP/3 connection, if the
+ * server has a known HTTP/3 endpoint.
+ * @param request the first request that will go over this connection
+ * @param h3client the HTTP/3 client
+ * @param exchange the exchange for which this connection is created
+ * @return a completable future that will be completed with a new
+ * HTTP/3 connection, or {@code null} if no usable HTTP/3 endpoint
+ * was found, or completed exceptionally if an error occurred
+ */
+ static CompletableFuture createAsync(HttpRequestImpl request,
+ Http3ClientImpl h3client,
+ Exchange> exchange) {
+ assert request.secure();
+ final HttpConnection connection = HttpConnection.getConnection(request.getAddress(),
+ h3client.client(),
+ exchange,
+ request,
+ HTTP_3);
+ var debug = h3client.debug();
+ var where = "Http3Connection.createAsync";
+ if (!(connection instanceof HttpQuicConnection httpQuicConnection)) {
+ if (Log.http3()) {
+ Log.logHttp3("{0}: Connection for {1} #{2} is not an HttpQuicConnection: {3}",
+ where, request, exchange.multi.id, connection);
+ }
+ if (debug.on())
+ debug.log("%s: Connection is not an HttpQuicConnection: %s", where, connection);
+ if (request.isHttp3Only(exchange.version())) {
+ assert connection == null;
+ // may happen if the client doesn't support HTTP3
+ return MinimalFuture.failedFuture(new UnsupportedProtocolVersionException(
+ "cannot establish exchange to requested origin with HTTP/3"));
+ }
+ return MinimalFuture.completedFuture(null);
+ }
+ if (debug.on()) {
+ debug.log("%s: Got HttpQuicConnection: %s", where, connection);
+ }
+ if (Log.http3()) {
+ Log.logHttp3("{0}: Got HttpQuicConnection for {1} #{2} is: {3}",
+ where, request, exchange.multi.id, connection.label());
+ }
+
+ // Expose the underlying connection to the exchange's aborter so it can
+ // be closed if a timeout occurs.
+ exchange.connectionAborter.connection(httpQuicConnection);
+
+ return httpQuicConnection.connectAsync(exchange)
+ .thenCompose(unused -> httpQuicConnection.finishConnect())
+ .thenCompose(unused -> checkSSLConfig(httpQuicConnection))
+ .thenCompose(notused-> {
+ CompletableFuture cf = new MinimalFuture<>();
+ try {
+ if (debug.on())
+ debug.log("creating Http3Connection for %s", httpQuicConnection);
+ Http3Connection hc = new Http3Connection(request, h3client, httpQuicConnection);
+ if (!hc.isFinalStream()) {
+ exchange.connectionAborter.clear(httpQuicConnection);
+ cf.complete(hc);
+ } else {
+ var io = new IOException("can't reserve first stream");
+ if (Log.http3()) {
+ Log.logHttp3(" Unable to use HTTP/3 connection over {0}: {1}",
+ hc.quicConnectionTag(),
+ io);
+ }
+ hc.protocolError(io);
+ cf.complete(null);
+ }
+ } catch (Exception e) {
+ cf.completeExceptionally(e);
+ }
+ return cf; } )
+ .whenComplete(httpQuicConnection::connectionEstablished);
+ }
+
+ private static CompletableFuture checkSSLConfig(HttpQuicConnection quic) {
+ // HTTP/2 checks ALPN here; with HTTP/3, we only offer one ALPN,
+ // and TLS verifies that it's negotiated.
+
+ // We can examine the negotiated parameters here and possibly fail
+ // if they are not satisfactory.
+ return MinimalFuture.completedFuture(null);
+ }
+
+ HttpQuicConnection connection() {
+ return connection;
+ }
+
+ String key() {
+ return connectionKey;
+ }
+
+ /**
+ * Whether the final stream (last stream allowed on a connection), has
+ * been set.
+ *
+ * @return true if the final stream has been set.
+ */
+ boolean isFinalStream() {
+ return this.finalStream;
+ }
+
+ /**
+ * Sets the final stream to be the next stream opened on
+ * the connection. No other stream will be opened after this.
+ */
+ void setFinalStream() {
+ this.finalStream = true;
+ }
+
+ void setFinalStreamAndCloseIfIdle() {
+ boolean closeNow;
+ lock();
+ try {
+ setFinalStream();
+ closeNow = finalStreamClosed();
+ } finally {
+ unlock();
+ }
+ if (closeNow) close();
+ }
+
+ void allowOnlyOneStream() {
+ lock();
+ try {
+ if (isFinalStream()) return;
+ this.allowOnlyOneStream = true;
+ this.finalStream = true;
+ } finally {
+ unlock();
+ }
+ }
+
+ boolean isOpen() {
+ return closedState == 0 && quicConnection.isOpen();
+ }
+
+ private IOException checkConnectionError() {
+ final TerminationCause tc = quicConnection.terminationCause();
+ return tc == null ? null : tc.getCloseCause();
+ }
+
+ // Used only by tests
+ CompletableFuture peerSettingsCF() {
+ return peerSettingsCF;
+ }
+
+ private boolean reserveStream() {
+ lock();
+ try {
+ boolean allowStream0 = this.allowOnlyOneStream;
+ this.allowOnlyOneStream = false;
+ if (finalStream && !allowStream0) {
+ return false;
+ }
+ reservedStreamCount.incrementAndGet();
+ return true;
+ } finally {
+ unlock();
+ }
+ }
+
+ CompletableFuture extends ExchangeImpl>
+ createStream(final Exchange exchange) throws IOException {
+ // check if this connection is closing before initiating this new stream
+ if (!reserveStream()) {
+ if (Log.http3()) {
+ Log.logHttp3("Cannot initiate new stream on connection {0} for exchange {1}",
+ quicConnectionTag(), exchange);
+ }
+ // we didn't create the stream and thus the server hasn't yet processed this request.
+ // mark the request as unprocessed to allow it to be retried on a different connection.
+ exchange.markUnprocessedByPeer();
+ String message = "cannot initiate additional new streams on chosen connection";
+ IOException cause = streamLimitReached
+ ? new StreamLimitException(HTTP_3, message)
+ : new IOException(message);
+ return MinimalFuture.failedFuture(cause);
+ }
+ // TODO: this duration is currently "computed" from the request timeout duration.
+ // this computation needs a bit more thought
+ final Duration streamLimitIncreaseDuration = exchange.request.timeout()
+ .map((reqTimeout) -> reqTimeout.dividedBy(2))
+ .orElse(Duration.ofMillis(MAX_STREAM_LIMIT_WAIT_TIMEOUT));
+ final CompletableFuture bidiStream =
+ quicConnection.openNewLocalBidiStream(streamLimitIncreaseDuration);
+ // once the bidi stream creation completes:
+ // - if completed exceptionally, we transform any QuicStreamLimitException into a
+ // StreamLimitException
+ // - if completed successfully, we create a Http3 exchange and return that as the result
+ final CompletableFuture>> h3ExchangeCf =
+ bidiStream.handle((stream, t) -> {
+ if (t == null) {
+ // no exception occurred and a bidi stream was created on the quic
+ // connection, but check if the connection has been terminated
+ // in the meantime
+ final var terminationCause = checkConnectionError();
+ if (terminationCause != null) {
+ // connection already closed and we haven't yet issued the request.
+ // mark the exchange as unprocessed to allow it to be retried on
+ // a different connection.
+ exchange.markUnprocessedByPeer();
+ return MinimalFuture.failedFuture(terminationCause);
+ }
+ // creation of bidi stream succeeded, now create the H3 exchange impl
+ // and return it
+ final Http3ExchangeImpl h3Exchange = createHttp3ExchangeImpl(exchange, stream);
+ return MinimalFuture.completedFuture(h3Exchange);
+ }
+ // failed to open a bidi stream
+ reservedStreamCount.decrementAndGet();
+ final Throwable cause = Utils.getCompletionCause(t);
+ if (cause instanceof QuicStreamLimitException) {
+ if (Log.http3()) {
+ Log.logHttp3("Maximum stream limit reached on {0} for exchange {1}",
+ quicConnectionTag(), exchange.multi.streamLimitState());
+ }
+ if (debug.on()) {
+ debug.log("bidi stream creation failed due to stream limit: "
+ + cause + ", connection will be marked as unusable for subsequent" +
+ " requests");
+ }
+ // Since we have reached the stream creation limit (which translates to not
+ // being able to initiate new requests on this connection), we mark the
+ // connection as "final stream" (i.e. don't consider this (pooled)
+ // connection for subsequent requests)
+ this.streamLimitReachedWith(exchange);
+ return MinimalFuture.failedFuture(new StreamLimitException(HTTP_3,
+ "No more streams allowed on connection"));
+ } else if (cause instanceof ClosedChannelException) {
+ // stream creation failed due to the connection (that was chosen)
+ // got closed. Thus the request wasn't processed by the server.
+ // mark the request as unprocessed to allow it to be
+ // initiated on a different connection
+ exchange.markUnprocessedByPeer();
+ return MinimalFuture.failedFuture(cause);
+ }
+ return MinimalFuture.failedFuture(cause);
+ });
+ return h3ExchangeCf.thenCompose(Function.identity());
+ }
+
+ private void streamLimitReachedWith(Exchange> exchange) {
+ streamLimitReached = true;
+ client.streamLimitReached(this, exchange.request);
+ setFinalStream();
+ }
+
+ private Http3ExchangeImpl createHttp3ExchangeImpl(Exchange exchange, QuicBidiStream stream) {
+ if (debug.on()) {
+ debug.log("Temporary reference h3 stream: " + stream.streamId());
+ }
+ if (Log.http3()) {
+ Log.logHttp3("Creating HTTP/3 exchange for {0}/streamId={1}",
+ quicConnectionTag(), Long.toString(stream.streamId()));
+ }
+ client.client.h3StreamReference();
+ try {
+ lock();
+ try {
+ this.exchangeStreams.put(stream.streamId(), stream);
+ reservedStreamCount.decrementAndGet();
+ var te = idleConnectionTimeoutEvent;
+ if (te != null) {
+ client.client().cancelTimer(te);
+ idleConnectionTimeoutEvent = null;
+ }
+ } finally {
+ unlock();
+ }
+ var http3Exchange = new Http3ExchangeImpl<>(this, exchange, stream);
+ return registerAndStartExchange(http3Exchange);
+ } finally {
+ if (debug.on()) {
+ debug.log("Temporary unreference h3 stream: " + stream.streamId());
+ }
+ client.client.h3StreamUnreference();
+ }
+ }
+
+ private Http3ExchangeImpl registerAndStartExchange(Http3ExchangeImpl exchange) {
+ var streamId = exchange.streamId();
+ if (debug.on()) debug.log("Reference h3 stream: " + streamId);
+ client.client.h3StreamReference();
+ exchanges.put(streamId, exchange);
+ exchange.start();
+ return exchange;
+ }
+
+ // marks this connection as no longer available for creating additional streams. current
+ // streams will run to completion. marking the connection as gracefully shutdown
+ // can involve sending the necessary protocol message(s) to the peer.
+ private void sendGoAway() throws IOException {
+ if (markSentGoAway()) {
+ // already sent (either successfully or an attempt was made) GOAWAY, nothing more to do
+ return;
+ }
+ // RFC-9114, section 5.2: Endpoints initiate the graceful shutdown of an HTTP/3 connection
+ // by sending a GOAWAY frame.
+ final QuicStreamWriter writer = controlStreamPair.localWriter();
+ if (writer != null && quicConnection.isOpen()) {
+ try {
+ // We send here the largest pushId for which the peer has
+ // opened a stream. We won't process pushIds larger than that, and
+ // we will later cancel any pending push promises anyway.
+ final long lastProcessedPushId = largestPushId.get();
+ final GoAwayFrame goAwayFrame = new GoAwayFrame(lastProcessedPushId);
+ final long size = goAwayFrame.size();
+ assert size >= 0 && size < Integer.MAX_VALUE;
+ final var buf = ByteBuffer.allocate((int) size);
+ goAwayFrame.writeFrame(buf);
+ buf.flip();
+ if (debug.on()) {
+ debug.log("Sending GOAWAY frame %s from client connection %s", goAwayFrame, this);
+ }
+ writer.scheduleForWriting(buf, false);
+ } catch (Exception e) {
+ // ignore - we couldn't send a GOAWAY
+ if (debug.on()) {
+ debug.log("Failed to send GOAWAY from client " + this, e);
+ }
+ Log.logError("Could not send a GOAWAY from client {0}", this);
+ Log.logError(e);
+ }
+ }
+ }
+
+ @Override
+ public void close() {
+ try {
+ sendGoAway();
+ } catch (IOException ioe) {
+ // log and ignore the failure
+ // failure to send a GOAWAY shouldn't prevent closing a connection
+ if (debug.on()) {
+ debug.log("failed to send a GOAWAY frame before initiating a close: " + ioe);
+ }
+ }
+ // TODO: ideally we should hava flushForClose() which goes all the way to terminator to flush
+ // streams and increasing the chances of GOAWAY being sent.
+ // check RFC-9114, section 5.3 which seems to allow including GOAWAY and CONNECTION_CLOSE
+ // frames in same packet (optionally)
+ close(Http3Error.H3_NO_ERROR, "H3 connection closed - no error");
+ }
+
+ void close(final Throwable throwable) {
+ close(H3_INTERNAL_ERROR, null, throwable);
+ }
+
+ void close(final Http3Error error, final String message) {
+ if (error != H3_NO_ERROR) {
+ // construct a ProtocolException representing the connection termination cause
+ final ProtocolException cause = new ProtocolException(message);
+ close(error, message, cause);
+ } else {
+ close(error, message, null);
+ }
+ }
+
+ void close(final Http3Error error, final String logMsg,
+ final Throwable closeCause) {
+ if (!markClosed()) {
+ // already closed, nothing to do
+ return;
+ }
+ if (debug.on()) {
+ debug.log("Closing HTTP/3 connection: %s %s %s", error, logMsg == null ? "" : logMsg,
+ closeCause == null ? "" : closeCause.toString());
+ debug.log("State is: " + describeClosedState(closedState));
+ }
+ exchanges.values().forEach(e -> e.recordError(closeCause));
+ // close the underlying QUIC connection
+ connection.close(error.code(), logMsg, closeCause);
+ final TerminationCause tc = connection.quicConnection.terminationCause();
+ assert tc != null : "termination cause is null";
+ // close all HTTP streams
+ exchanges.values().forEach(exchange -> exchange.cancelImpl(tc.getCloseCause(), error));
+ pushManager.cancelAllPromises(tc.getCloseCause(), error);
+ discardConnectionState();
+ // No longer wait for reading HTTP/3 stream types:
+ // stop waiting on any stream for which we haven't received the stream
+ // type yet.
+ try {
+ var listener = remoteStreamListener;
+ if (listener != null) {
+ quicConnection.removeRemoteStreamListener(listener);
+ }
+ } finally {
+ client.connectionClosed(this);
+ }
+ if (!peerSettingsCF.isDone()) {
+ peerSettingsCF.completeExceptionally(tc.getCloseCause());
+ }
+ }
+
+ private void discardConnectionState() {
+ controlStreamPair.stopSchedulers();
+ controlFramesDecoder.clear();
+ qpackDecoderStreams.stopSchedulers();
+ qpackEncoderStreams.stopSchedulers();
+ }
+
+ private boolean markClosed() {
+ return markClosedState(CLOSED);
+ }
+
+ void protocolError(IOException error) {
+ connectionError(error, Http3Error.H3_GENERAL_PROTOCOL_ERROR);
+ }
+
+ void connectionError(Throwable throwable, Http3Error error) {
+ connectionError(null, throwable, error.code(), null);
+ }
+
+ void connectionError(Http3Stream> exchange, Throwable throwable, long errorCode,
+ String logMsg) {
+ final Optional error = Http3Error.fromCode(errorCode);
+ assert error.isPresent() : "not a HTTP3 error code: " + errorCode;
+ close(error.get(), logMsg, throwable);
+ }
+
+ public String toString() {
+ return String.format("Http3Connection(%s)", connection());
+ }
+
+ private boolean finalStreamClosed() {
+ lock();
+ try {
+ return this.finalStream && this.exchangeStreams.isEmpty() && this.reservedStreamCount.get() == 0;
+ } finally {
+ unlock();
+ }
+ }
+
+ /**
+ * Called by the {@link Http3ExchangeImpl} when the exchange is closed.
+ *
+ * @param streamId The request stream id
+ */
+ void onExchangeClose(Http3ExchangeImpl> exch, final long streamId) {
+ // we expect it to be a request/response stream
+ if (!(QuicStreams.isClientInitiated(streamId) && QuicStreams.isBidirectional(streamId))) {
+ throw new IllegalArgumentException("Not a client initiated bidirectional stream");
+ }
+ if (this.exchangeStreams.remove(streamId) != null) {
+ if (connection().quicConnection().isOpen()) {
+ qpackDecoder.cancelStream(streamId);
+ }
+ decrementStreamsCount(exch, streamId);
+ exchanges.remove(streamId);
+ }
+
+ if (finalStreamClosed()) {
+ // no more streams open on this connection. close the connection
+ if (Log.http3()) {
+ Log.logHttp3("Closing HTTP/3 connection {0} on final stream (streamId={1})",
+ quicConnectionTag(), Long.toString(streamId));
+ }
+ // close will take care of canceling all pending push promises
+ // if any push promises are left pending
+ close();
+ } else {
+ if (Log.http3()) {
+ Log.logHttp3("HTTP/3 connection {0} left open: exchanged streamId={1} closed; " +
+ "finalStream={2}, exchangeStreams={3}, reservedStreamCount={4}",
+ quicConnectionTag(), Long.toString(streamId), finalStream,
+ exchangeStreams.size(), reservedStreamCount.get());
+ }
+ lock();
+ try {
+ var te = idleConnectionTimeoutEvent;
+ if (te == null && exchangeStreams.isEmpty()) {
+ te = idleConnectionTimeoutEvent = client.client().idleConnectionTimeout(HTTP_3)
+ .map(IdleConnectionTimeoutEvent::new).orElse(null);
+ if (te != null) {
+ client.client().registerTimer(te);
+ }
+ }
+ } finally {
+ unlock();
+ }
+ }
+ }
+
+ void decrementStreamsCount(Http3ExchangeImpl> exch, long streamid) {
+ if (exch.deRegister()) {
+ debug.log("Unreference h3 stream: " + streamid);
+ client.client.h3StreamUnreference();
+ } else {
+ debug.log("Already unreferenced h3 stream: " + streamid);
+ }
+ }
+
+ // Called from Http3PushPromiseStream::start (via Http3ExchangeImpl)
+ void onPushPromiseStreamStarted(Http3PushPromiseStream http3PushPromiseStream, long streamId) {
+ // HTTP/3 push promises are not refcounted.
+ // At the moment an ongoing push promise will not prevent the client
+ // to exit normally, if all request-response streams are finished.
+ // Here would be the place to increment ref-counting if we wanted to
+ }
+
+ // Called by Http3PushPromiseStream::close
+ void onPushPromiseStreamClosed(Http3PushPromiseStream http3PushPromiseStream, long streamId) {
+ // HTTP/3 push promises are not refcounted.
+ // At the moment an ongoing push promise will not prevent the client
+ // to exit normally, if all request-response streams are finished.
+ // Here would be the place to decrement ref-counting if we wanted to
+ if (connection().quicConnection().isOpen()) {
+ qpackDecoder.cancelStream(streamId);
+ }
+ }
+
+ /**
+ * A class used to dispatch peer initiated unidirectional streams
+ * according to their HTTP/3 stream type.
+ * The type of an HTTP/3 unidirectional stream is determined by
+ * reading a variable length integer code off the stream, which
+ * indicates the type of stream.
+ * @see Http3Streams
+ */
+ private final class Http3StreamDispatcher extends PeerUniStreamDispatcher {
+ Http3StreamDispatcher(QuicReceiverStream stream) {
+ super(stream);
+ }
+
+ @Override
+ protected Logger debug() { return debug; }
+
+ @Override
+ protected void onStreamAbandoned(QuicReceiverStream stream) {
+ if (debug.on()) debug.log("Stream " + stream.streamId() + " abandoned!");
+ qpackDecoder.cancelStream(stream.streamId());
+ }
+
+ @Override
+ protected void onControlStreamCreated(String description, QuicReceiverStream stream) {
+ complete(description, stream, controlStreamPair.futureReceiverStream());
+ }
+
+ @Override
+ protected void onEncoderStreamCreated(String description, QuicReceiverStream stream) {
+ complete(description, stream, qpackDecoderStreams.futureReceiverStream());
+ }
+
+ @Override
+ protected void onDecoderStreamCreated(String description, QuicReceiverStream stream) {
+ complete(description, stream, qpackEncoderStreams.futureReceiverStream());
+ }
+
+ @Override
+ protected void onPushStreamCreated(String description, QuicReceiverStream stream, long pushId) {
+ Http3Connection.this.onPushStreamCreated(stream, pushId);
+ }
+
+ // completes the given completable future with the given stream
+ private void complete(String description, QuicReceiverStream stream, CompletableFuture cf) {
+ debug.log("completing CF for %s with stream %s", description, stream.streamId());
+ boolean completed = cf.complete(stream);
+ if (!completed) {
+ if (!cf.isCompletedExceptionally()) {
+ debug.log("CF for %s already completed with stream %s!", description, cf.resultNow().streamId());
+ close(Http3Error.H3_STREAM_CREATION_ERROR,
+ "%s already created".formatted(description));
+ } else {
+ debug.log("CF for %s already completed exceptionally!", description);
+ }
+ }
+ }
+
+ /**
+ * Dispatches the given remote initiated unidirectional stream to the
+ * given Http3Connection after reading the stream type off the stream.
+ *
+ * @param conn the Http3Connection with which the stream is associated
+ * @param stream a newly opened remote unidirectional stream.
+ */
+ static CompletableFuture dispatch(Http3Connection conn, QuicReceiverStream stream) {
+ assert stream.isRemoteInitiated();
+ assert !stream.isBidirectional();
+ var dispatcher = conn.new Http3StreamDispatcher(stream);
+ dispatcher.start();
+ return dispatcher.dispatchCF();
+ }
+ }
+
+ /**
+ * Attempts to notify the idle connection management that this connection should
+ * be considered "in use". This way the idle connection management doesn't close
+ * this connection during the time the connection is handed out from the pool and any
+ * new stream created on that connection.
+ *
+ * @return true if the connection has been successfully reserved and is {@link #isOpen()}. false
+ * otherwise; in which case the connection must not be handed out from the pool.
+ */
+ boolean tryReserveForPoolCheckout() {
+ // must be done with "stateLock" held to co-ordinate idle connection management
+ lock();
+ try {
+ cancelIdleShutdownEvent();
+ // co-ordinate with the QUIC connection to prevent it from silently terminating
+ // a potentially idle transport
+ if (!quicConnection.connectionTerminator().tryReserveForUse()) {
+ // QUIC says the connection can't be used
+ return false;
+ }
+ // consider the reservation successful only if the connection's state hasn't moved
+ // to "being closed"
+ return isOpen() && finalStream == false;
+ } finally {
+ unlock();
+ }
+ }
+
+ /**
+ * Cancels any event that might have been scheduled to shutdown this connection. Must be called
+ * with the stateLock held.
+ */
+ private void cancelIdleShutdownEvent() {
+ assert lock.isHeldByCurrentThread() : "Current thread doesn't hold " + lock;
+ if (idleConnectionTimeoutEvent == null) return;
+ idleConnectionTimeoutEvent.cancel();
+ idleConnectionTimeoutEvent = null;
+ }
+
+ // An Idle connection is one that has no active streams
+ // and has not sent the final stream flag
+ final class IdleConnectionTimeoutEvent extends TimeoutEvent {
+
+ // both cancelled and idleShutDownInitiated are to be accessed
+ // when holding the connection's lock
+ private boolean cancelled;
+ private boolean idleShutDownInitiated;
+
+ IdleConnectionTimeoutEvent(Duration duration) {
+ super(duration);
+ }
+
+ @Override
+ public void handle() {
+ boolean okToIdleTimeout;
+ lock();
+ try {
+ if (cancelled || idleShutDownInitiated) {
+ return;
+ }
+ idleShutDownInitiated = true;
+ if (debug.on()) {
+ debug.log("H3 idle shutdown initiated");
+ }
+ setFinalStream();
+ okToIdleTimeout = finalStreamClosed();
+ } finally {
+ unlock();
+ }
+ if (okToIdleTimeout) {
+ if (debug.on()) {
+ debug.log("closing idle H3 connection");
+ }
+ close();
+ }
+ }
+
+ /**
+ * Cancels this event. Should be called with stateLock held
+ */
+ void cancel() {
+ assert lock.isHeldByCurrentThread() : "Current thread doesn't hold " + lock;
+ // mark as cancelled to prevent potentially already triggered event from actually
+ // doing the shutdown
+ this.cancelled = true;
+ // cancel the timer to prevent the event from being triggered (if it hasn't already)
+ client.client().cancelTimer(this);
+ }
+
+ @Override
+ public String toString() {
+ return "IdleConnectionTimeoutEvent, " + super.toString();
+ }
+
+ }
+
+ /**
+ * This method is called when the peer opens a new stream.
+ * The stream can be unidirectional or bidirectional.
+ *
+ * @param stream the new stream
+ * @return always returns true (see {@link
+ * QuicConnection#addRemoteStreamListener(Predicate)}
+ */
+ private boolean onOpenRemoteStream(QuicReceiverStream stream) {
+ debug.log("on open remote stream: " + stream.streamId());
+ if (stream instanceof QuicBidiStream bidi) {
+ // A server will never open a bidirectional stream
+ // with the client. A client opens a new bidirectional
+ // stream for each request/response exchange.
+ return onRemoteBidirectionalStream(bidi);
+ } else {
+ // Four types of unidirectional stream are defined:
+ // control stream, qpack encoder, qpack decoder, push
+ // promise stream
+ return onRemoteUnidirectionalStream(stream);
+ }
+ }
+
+ /**
+ * This method is called when the peer opens a unidirectional stream.
+ *
+ * @param uni the unidirectional stream opened by the peer
+ * @return always returns true ({@link
+ * QuicConnection#addRemoteStreamListener(Predicate)}
+ */
+ protected boolean onRemoteUnidirectionalStream(QuicReceiverStream uni) {
+ assert !uni.isBidirectional();
+ assert uni.isRemoteInitiated();
+ if (!isOpen()) return false;
+ debug.log("dispatching unidirectional remote stream: " + uni.streamId());
+ Http3StreamDispatcher.dispatch(this, uni).whenComplete((r, t)-> {
+ if (t!=null) this.dispatchingFailed(uni, t);
+ });
+ return true;
+ }
+
+ /**
+ * Called when the peer opens a bidirectional stream.
+ * On the client side, this method should never be called.
+ *
+ * @param bidi the new bidirectional stream opened by the
+ * peer.
+ * @return always returns false ({@link
+ * QuicConnection#addRemoteStreamListener(Predicate)}
+ */
+ protected boolean onRemoteBidirectionalStream(QuicBidiStream bidi) {
+ assert bidi.isRemoteInitiated();
+ assert bidi.isBidirectional();
+
+ // From RFC 9114, Section 6.1:
+ // Clients MUST treat receipt of a server-initiated bidirectional
+ // stream as a connection error of type H3_STREAM_CREATION_ERROR
+ // [ unless such an extension has been negotiated].
+ // We don't support any extension, so this is a connection error.
+ close(Http3Error.H3_STREAM_CREATION_ERROR,
+ "Bidirectional stream %s opened by server peer"
+ .formatted(bidi.streamId()));
+ return false;
+ }
+
+ /**
+ * Called if the dispatch failed.
+ *
+ * @param reason the reason of the failure
+ */
+ protected void dispatchingFailed(QuicReceiverStream uni, Throwable reason) {
+ debug.log("dispatching failed for streamId=%s: %s", uni.streamId(), reason);
+ close(H3_STREAM_CREATION_ERROR, "failed to dispatch remote stream " + uni.streamId(), reason);
+ }
+
+
+ /**
+ * Schedules sending of client settings.
+ *
+ * @return a completable future that will be completed with the
+ * {@link QuicStreamWriter} allowing to write to the local control
+ * stream
+ */
+ QuicStreamWriter sendSettings(QuicStreamWriter writer) {
+ try {
+ final SettingsFrame settings = QPACK.updateDecoderSettings(SettingsFrame.defaultRFCSettings());
+ this.ourSettings = ConnectionSettings.createFrom(settings);
+ this.qpackDecoder.configure(ourSettings);
+ if (debug.on()) {
+ debug.log("Sending client settings %s for connection %s", this.ourSettings, this);
+ }
+ long size = settings.size();
+ assert size >= 0 && size < Integer.MAX_VALUE;
+ var buf = ByteBuffer.allocate((int) size);
+ settings.writeFrame(buf);
+ buf.flip();
+ writer.scheduleForWriting(buf, false);
+ return writer;
+ } catch (IOException io) {
+ throw new CompletionException(io);
+ }
+ }
+
+ /**
+ * Schedules sending of max push id that this (client) connection allows.
+ *
+ * @param writer the control stream writer
+ * @return the {@link QuicStreamWriter} passed as parameter
+ */
+ private QuicStreamWriter sendMaxPushId(QuicStreamWriter writer) {
+ try {
+ long maxPushId = pushManager.getMaxPushId();
+ if (maxPushId > 0 && maxPushId > maxPushIdSent.get()) {
+ return sendMaxPushId(writer, maxPushId);
+ } else {
+ return writer;
+ }
+ } catch (IOException io) {
+ // will wrap the io exception in CompletionException,
+ // close the connection, and throw.
+ throw new CompletionException(io);
+ }
+ }
+
+ // local control stream write loop
+ void lcsWriterLoop() {
+ // since we do not write much data on the control stream
+ // we don't check for credit and always directly buffer
+ // the data to send in the writer. Therefore, there is
+ // nothing to do in the control stream writer loop.
+ //
+ // When more credit is available, check if we need
+ // to send maxpushid;
+ if (maxPushIdSent.get() < pushManager.getMaxPushId()) {
+ var writer = controlStreamPair.localWriter();
+ if (writer != null && writer.connected()) {
+ sendMaxPushId(writer);
+ }
+ }
+ }
+
+ void controlStreamFailed(final QuicStream stream, final UniStreamPair uniStreamPair,
+ final Throwable throwable) {
+ Http3Streams.debugErrorCode(debug, stream, "Control stream failed");
+ if (stream.state() instanceof QuicReceiverStream.ReceivingStreamState rcvrStrmState) {
+ if (rcvrStrmState.isReset() && quicConnection.isOpen()) {
+ // RFC-9114, section 6.2.1:
+ // If either control stream is closed at any point,
+ // this MUST be treated as a connection error of type H3_CLOSED_CRITICAL_STREAM.
+ final String logMsg = "control stream " + stream.streamId()
+ + " was reset";
+ close(H3_CLOSED_CRITICAL_STREAM, logMsg);
+ return;
+ }
+ }
+ if (isOpen()) {
+ if (debug.on()) {
+ debug.log("closing connection since control stream " + stream.mode()
+ + " failed", throwable);
+ }
+ }
+ close(throwable);
+ }
+
+ /**
+ * This method is called to process bytes received on the peer
+ * control stream.
+ *
+ * @param buffer the bytes received
+ */
+ private void processPeerControlBytes(final ByteBuffer buffer) {
+ debug.log("received server control: %s bytes", buffer.remaining());
+ controlFramesDecoder.submit(buffer);
+ Http3Frame frame;
+ while ((frame = controlFramesDecoder.poll()) != null) {
+ final long frameType = frame.type();
+ debug.log("server control frame: %s", Http3FrameType.asString(frameType));
+ if (frame instanceof MalformedFrame malformed) {
+ var cause = malformed.getCause();
+ if (cause != null && debug.on()) {
+ debug.log(malformed.toString(), cause);
+ }
+ final Http3Error error = Http3Error.fromCode(malformed.getErrorCode())
+ .orElse(H3_INTERNAL_ERROR);
+ close(error, malformed.getMessage());
+ controlStreamPair.stopSchedulers();
+ controlFramesDecoder.clear();
+ return;
+ }
+ final boolean settingsRcvd = this.settingsFrameReceived;
+ if ((frameType == SettingsFrame.TYPE && settingsRcvd)
+ || !this.frameOrderVerifier.allowsProcessing(frame)) {
+ final String unexpectedFrameType = Http3FrameType.asString(frameType);
+ // not expected to be arriving now, we either use H3_FRAME_UNEXPECTED
+ // or H3_MISSING_SETTINGS for the connection error, depending on the context.
+ //
+ // RFC-9114, section 4.1: Receipt of an invalid sequence of frames MUST be
+ // treated as a connection error of type H3_FRAME_UNEXPECTED.
+ //
+ // RFC-9114, section 6.2.1: If the first frame of the control stream
+ // is any other frame type, this MUST be treated as a connection error of
+ // type H3_MISSING_SETTINGS.
+ final String logMsg = "unexpected (order of) frame type: " + unexpectedFrameType
+ + " on control stream";
+ if (!settingsRcvd) {
+ close(Http3Error.H3_MISSING_SETTINGS, logMsg);
+ } else {
+ close(Http3Error.H3_FRAME_UNEXPECTED, logMsg);
+ }
+ controlStreamPair.stopSchedulers();
+ controlFramesDecoder.clear();
+ return;
+ }
+ if (frame instanceof SettingsFrame settingsFrame) {
+ this.settingsFrameReceived = true;
+ this.peerSettings = ConnectionSettings.createFrom(settingsFrame);
+ if (debug.on()) {
+ debug.log("Received peer settings %s for connection %s", this.peerSettings, this);
+ }
+ peerSettingsCF.completeAsync(() -> peerSettings,
+ client.client().theExecutor().safeDelegate());
+ // We can only initialize encoder's DT only when we get Settings frame with all parameters
+ qpackEncoder().configure(peerSettings);
+ }
+ if (frame instanceof CancelPushFrame cancelPush) {
+ pushManager.cancelPushPromise(cancelPush.getPushId(), null, CancelPushReason.CANCEL_RECEIVED);
+ }
+ if (frame instanceof GoAwayFrame goaway) {
+ handleIncomingGoAway(goaway);
+ }
+ if (frame instanceof PartialFrame partial) {
+ var payloadBytes = controlFramesDecoder.readPayloadBytes();
+ debug.log("added %s bytes to %s",
+ payloadBytes == null ? 0 : Utils.remaining(payloadBytes),
+ frame);
+ if (partial.remaining() == 0) {
+ this.frameOrderVerifier.completed(frame);
+ } else if (payloadBytes == null || payloadBytes.isEmpty()) {
+ break;
+ }
+ // only reserved frames reach here; just drop the payload
+ } else {
+ this.frameOrderVerifier.completed(frame);
+ }
+ if (controlFramesDecoder.eof()) {
+ break;
+ }
+ }
+ if (controlFramesDecoder.eof()) {
+ close(H3_CLOSED_CRITICAL_STREAM, "EOF reached while reading server control stream");
+ }
+ }
+
+ /**
+ * Called when a new push promise stream is created by the peer.
+ *
+ * @apiNote this method gives an opportunity to cancel the stream
+ * before reading the pushId, if it is known that no push
+ * will be accepted anyway.
+ *
+ * @param pushStream the new push promise stream
+ * @param pushId or -1 if the pushId is not available yet
+ */
+ private void onPushStreamCreated(QuicReceiverStream pushStream, long pushId) {
+ assert pushStream.isRemoteInitiated();
+ assert !pushStream.isBidirectional();
+
+ onPushPromiseStream(pushStream, pushId);
+ }
+
+ /**
+ * Called when a new push promise stream is created by the peer, and
+ * the pushId has been read.
+ *
+ * @param pushStream the new push promise stream
+ * @param pushId the pushId
+ */
+ void onPushPromiseStream(QuicReceiverStream pushStream, long pushId) {
+ assert pushId >= 0;
+ pushManager.onPushPromiseStream(pushStream, pushId);
+ }
+
+ /**
+ * This method is called by the {@link Http3PushManager} to figure out whether
+ * a push stream or a push promise should be processed, with respect to the
+ * GOAWAY state. Any pushId larger than what was sent in the GOAWAY frame
+ * should be cancelled /rejected.
+ *
+ * @param pushStream a push stream (may be null if not yet materialized)
+ * @param pushId a pushId, must be > 0
+ * @return true if the pushId can be processed
+ */
+ boolean acceptLargerPushPromise(QuicReceiverStream pushStream, long pushId) {
+ // if GOAWAY has been sent, just cancel the push promise
+ // otherwise - track this as the maxPushId that will be
+ // sent in GOAWAY
+ if (checkMaxPushId(pushId) != null) return false; // connection will be closed
+ while (true) {
+ long largestPushId = this.largestPushId.get();
+ if ((closedState & GOAWAY_SENT) == GOAWAY_SENT) {
+ if (pushId >= largestPushId) {
+ if (pushStream != null) {
+ pushStream.requestStopSending(H3_NO_ERROR.code());
+ }
+ pushManager.cancelPushPromise(pushId, null, CancelPushReason.PUSH_CANCELLED);
+ return false;
+ }
+ }
+ if (pushId <= largestPushId) break;
+ if (!this.largestPushId.compareAndSet(largestPushId, pushId)) continue;
+ if ((closedState & GOAWAY_SENT) == 0) break;
+ }
+ // If we reach here, then either GOAWAY has been sent with a largestPushId >= pushId,
+ // or GOAWAY has not been sent yet.
+ return true;
+ }
+
+ QueuingStreamPair createEncoderStreams(Consumer encoderReceiver) {
+ return new QueuingStreamPair(StreamType.QPACK_ENCODER, quicConnection,
+ encoderReceiver, this::onEncoderStreamsFailed, debug);
+ }
+
+ private void onEncoderStreamsFailed(final QuicStream stream, final UniStreamPair uniStreamPair,
+ final Throwable throwable) {
+ Http3Streams.debugErrorCode(debug, stream, "Encoder stream failed");
+ if (stream.state() instanceof QuicReceiverStream.ReceivingStreamState rcvrStrmState) {
+ if (rcvrStrmState.isReset() && quicConnection.isOpen()) {
+ // RFC-9204, section 4.2:
+ // Closure of either unidirectional stream type MUST be treated as a connection
+ // error of type H3_CLOSED_CRITICAL_STREAM.
+ final String logMsg = "QPACK encoder stream " + stream.streamId()
+ + " was reset";
+ close(H3_CLOSED_CRITICAL_STREAM, logMsg);
+ return;
+ }
+ }
+ if (isOpen()) {
+ if (debug.on()) {
+ debug.log("closing connection since QPack encoder stream " + stream.streamId()
+ + " failed", throwable);
+ }
+ }
+ close(throwable);
+ }
+
+ QueuingStreamPair createDecoderStreams(Consumer encoderReceiver) {
+ return new QueuingStreamPair(StreamType.QPACK_DECODER, quicConnection,
+ encoderReceiver, this::onDecoderStreamsFailed, debug);
+ }
+
+ private void onDecoderStreamsFailed(final QuicStream stream, final UniStreamPair uniStreamPair,
+ final Throwable throwable) {
+ Http3Streams.debugErrorCode(debug, stream, "Decoder stream failed");
+ if (stream.state() instanceof QuicReceiverStream.ReceivingStreamState rcvrStrmState) {
+ if (rcvrStrmState.isReset() && quicConnection.isOpen()) {
+ // RFC-9204, section 4.2:
+ // Closure of either unidirectional stream type MUST be treated as a connection
+ // error of type H3_CLOSED_CRITICAL_STREAM.
+ final String logMsg = "QPACK decoder stream " + stream.streamId()
+ + " was reset";
+ close(H3_CLOSED_CRITICAL_STREAM, logMsg);
+ return;
+ }
+ }
+ if (isOpen()) {
+ if (debug.on()) {
+ debug.log("closing connection since QPack decoder stream " + stream.streamId()
+ + " failed", throwable);
+ }
+ }
+ close(throwable);
+ }
+
+ // This method never returns anything: it always throws
+ private T exceptionallyAndClose(Throwable t) {
+ try {
+ return exceptionally(t);
+ } finally {
+ close(t);
+ }
+ }
+
+ // This method never returns anything: it always throws
+ private T exceptionally(Throwable t) {
+ try {
+ debug.log(t.getMessage(), t);
+ throw t;
+ } catch (RuntimeException | Error r) {
+ throw r;
+ } catch (ExecutionException x) {
+ throw new CompletionException(x.getMessage(), x.getCause());
+ } catch (Throwable e) {
+ throw new CompletionException(e.getMessage(), e);
+ }
+ }
+
+ Decoder qpackDecoder() {
+ return qpackDecoder;
+ }
+
+ Encoder qpackEncoder() {
+ return qpackEncoder;
+ }
+
+ /**
+ * {@return the settings, sent by the peer, for this connection. If none is present, due to the SETTINGS
+ * frame not yet arriving from the peer, this method returns {@link Optional#empty()}}
+ */
+ Optional getPeerSettings() {
+ return Optional.ofNullable(this.peerSettings);
+ }
+
+ private void handleIncomingGoAway(final GoAwayFrame incomingGoAway) {
+ final long quicStreamId = incomingGoAway.getTargetId();
+ if (debug.on()) {
+ debug.log("Received GOAWAY %s", incomingGoAway);
+ }
+ // ensure request stream id is a bidirectional stream originating from the client.
+ // RFC-9114, section 7.2.6: A client MUST treat receipt of a GOAWAY frame containing
+ // a stream ID of any other type as a connection error of type H3_ID_ERROR.
+ if (!(QuicStreams.isClientInitiated(quicStreamId)
+ && QuicStreams.isBidirectional(quicStreamId))) {
+ close(Http3Error.H3_ID_ERROR, "Invalid stream id in GOAWAY frame");
+ return;
+ }
+ boolean validStreamId = false;
+ long current = lowestGoAwayReceipt.get();
+ while (current == -1 || quicStreamId <= current) {
+ if (lowestGoAwayReceipt.compareAndSet(current, quicStreamId)) {
+ validStreamId = true;
+ break;
+ }
+ current = lowestGoAwayReceipt.get();
+ }
+ if (!validStreamId) {
+ // the request stream id received in the GOAWAY frame is greater than the one received
+ // in some previous GOAWAY frame. This isn't allowed by spec.
+ // RFC-9114, section 5.2: An endpoint MAY send multiple GOAWAY frames indicating
+ // different identifiers, but the identifier in each frame MUST NOT be greater than
+ // the identifier in any previous frame, ... Receiving a GOAWAY containing a larger
+ // identifier than previously received MUST be treated as a connection error of
+ // type H3_ID_ERROR.
+ close(Http3Error.H3_ID_ERROR, "Invalid stream id in newer GOAWAY frame");
+ return;
+ }
+ markReceivedGoAway();
+ // mark a state on this connection to let it know that no new streams are allowed on this
+ // connection.
+ // RFC-9114, section 5.2: Endpoints MUST NOT initiate new requests or promise new pushes on
+ // the connection after receipt of a GOAWAY frame from the peer.
+ setFinalStream();
+ if (debug.on()) {
+ debug.log("Connection will no longer allow new streams due to receipt of GOAWAY" +
+ " from peer");
+ }
+ handlePeerUnprocessedStreams(quicStreamId);
+ if (finalStreamClosed()) {
+ close(Http3Error.H3_NO_ERROR, "GOAWAY received");
+ }
+ }
+
+ private void handlePeerUnprocessedStreams(final long leastUnprocessedStreamId) {
+ this.exchanges.forEach((id, exchange) -> {
+ if (id >= leastUnprocessedStreamId) {
+ // close the exchange as unprocessed
+ client.client().theExecutor().execute(exchange::closeAsUnprocessed);
+ }
+ });
+ }
+
+ private boolean isMarked(int state, int mask) {
+ return (state & mask) == mask;
+ }
+
+ private boolean markSentGoAway() {
+ return markClosedState(GOAWAY_SENT);
+ }
+
+ private boolean markReceivedGoAway() {
+ return markClosedState(GOAWAY_RECEIVED);
+ }
+
+ private boolean markClosedState(int flag) {
+ int state, desired;
+ do {
+ state = closedState;
+ if ((state & flag) == flag) return false;
+ desired = state | flag;
+ } while (!CLOSED_STATE.compareAndSet(this, state, desired));
+ return true;
+ }
+
+ String describeClosedState(int state) {
+ if (state == 0) return "active";
+ String desc = null;
+ if (isMarked(state, GOAWAY_SENT)) {
+ if (desc == null) desc = "goaway-sent";
+ else desc += "+goaway-sent";
+ }
+ if (isMarked(state, GOAWAY_RECEIVED)) {
+ if (desc == null) desc = "goaway-rcvd";
+ else desc += "+goaway-rcvd";
+ }
+ if (isMarked(state, CLOSED)) {
+ if (desc == null) desc = "quic-closed";
+ else desc += "+quic-closed";
+ }
+ return desc != null ? desc : "0x" + Integer.toHexString(state);
+ }
+
+ // PushPromise handling
+ // ====================
+
+ /**
+ * {@return a new PushId for the given pushId}
+ * @param pushId the pushId
+ */
+ PushId newPushId(long pushId) {
+ return new Http3PushId(pushId, connection.label());
+ }
+
+ /**
+ * Called when a pushId needs to be cancelled.
+ * @param pushId the pushId to cancel
+ * @param cause the cause (may be {@code null}).
+ */
+ void pushCancelled(long pushId, Throwable cause) {
+ pushManager.cancelPushPromise(pushId, cause, CancelPushReason.PUSH_CANCELLED);
+ }
+
+ /**
+ * Called if a PushPromiseFrame is received by an exchange that doesn't have any
+ * {@link java.net.http.HttpResponse.PushPromiseHandler}. The pushId will be
+ * cancelled, unless it's already been accepted by another exchange.
+ *
+ * @param pushId the pushId
+ */
+ void noPushHandlerFor(long pushId) {
+ pushManager.cancelPushPromise(pushId, null, CancelPushReason.NO_HANDLER);
+ }
+
+ boolean acceptPromises() {
+ return exchanges.values().stream().anyMatch(Http3ExchangeImpl::acceptPushPromise);
+ }
+
+ /**
+ * {@return a completable future that will be completed when a pushId has been
+ * accepted by the exchange in charge of creating the response body}
+ *
+ * The completable future is complete with {@code true} if the pushId is
+ * accepted, and with {@code false} if the pushId was rejected or cancelled.
+ *
+ * @apiNote
+ * This method is intended to be called when {@link
+ * #onPushPromiseFrame(Http3ExchangeImpl, long, HttpHeaders)}, returns false,
+ * indicating that the push promise is being delegated to another request/response
+ * exchange.
+ * On completion of the future returned here, if the future is completed
+ * with {@code true}, the caller is expected to call {@link
+ * PushGroup#acceptPushPromiseId(PushId)} in order to notify the {@link
+ * java.net.http.HttpResponse.PushPromiseHandler} of the received {@code pushId}.
+ *
+ * Callers should not forward the pushId to a {@link
+ * java.net.http.HttpResponse.PushPromiseHandler} unless the future is completed
+ * with {@code true}
+ *
+ * @param pushId the pushId
+ */
+ CompletableFuture whenPushAccepted(long pushId) {
+ return pushManager.whenAccepted(pushId);
+ }
+
+ /**
+ * Called when a PushPromiseFrame has been decoded.
+ *
+ * @param exchange The HTTP/3 exchange that received the frame
+ * @param pushId The pushId contained in the frame
+ * @param promiseHeaders The push promise headers contained in the frame
+ * @return true if the exchange should take care of creating the HttpResponse body,
+ * false otherwise
+ */
+ boolean onPushPromiseFrame(Http3ExchangeImpl> exchange, long pushId, HttpHeaders promiseHeaders)
+ throws IOException {
+ return pushManager.onPushPromiseFrame(exchange, pushId, promiseHeaders);
+ }
+
+ /**
+ * Checks whether a MAX_PUSH_ID frame should be sent.
+ */
+ void checkSendMaxPushId() {
+ pushManager.checkSendMaxPushId();
+ }
+
+ /**
+ * Schedules sending of max push id that this (client) connection allows.
+ *
+ * @return a completable future that will be completed with the
+ * {@link QuicStreamWriter} allowing to write to the local control
+ * stream
+ */
+ private QuicStreamWriter sendMaxPushId(QuicStreamWriter writer, long maxPushId) throws IOException {
+ debug.log("Sending max push id frame with max push id set to " + maxPushId);
+ final MaxPushIdFrame maxPushIdFrame = new MaxPushIdFrame(maxPushId);
+ final long frameSize = maxPushIdFrame.size();
+ assert frameSize >= 0 && frameSize < Integer.MAX_VALUE;
+ final ByteBuffer buf = ByteBuffer.allocate((int) frameSize);
+ maxPushIdFrame.writeFrame(buf);
+ buf.flip();
+ if (writer.credit() > buf.remaining()) {
+ long previous;
+ do {
+ previous = maxPushIdSent.get();
+ if (previous >= maxPushId) return writer;
+ } while (!maxPushIdSent.compareAndSet(previous, maxPushId));
+ writer.scheduleForWriting(buf, false);
+ }
+ return writer;
+ }
+
+ /**
+ * Send a MAX_PUSH_ID frame on the control stream with the given {@code maxPushId}
+ *
+ * @param maxPushId the new maxPushId
+ *
+ * @throws IOException if the pushId could not be sent
+ */
+ void sendMaxPushId(long maxPushId) throws IOException {
+ sendMaxPushId(controlStreamPair.localWriter(), maxPushId);
+ }
+
+ /**
+ * Sends a CANCEL_PUSH frame for the given {@code pushId}.
+ * If not null, the cause may indicate why the push is cancelled.
+ *
+ * @apiNote the cause is only used for logging
+ *
+ * @param pushId the pushId to cancel
+ * @param cause the reason for cancelling, may be {@code null}
+ */
+ void sendCancelPush(long pushId, Throwable cause) {
+ // send CANCEL_PUSH frame here
+ if (debug.on()) {
+ if (cause != null) {
+ debug.log("Push Promise %s cancelled: %s", pushId, cause.getMessage());
+ } else {
+ debug.log("Push Promise %s cancelled", pushId);
+ }
+ }
+ try {
+ CancelPushFrame cancelPush = new CancelPushFrame(pushId);
+ long size = cancelPush.size();
+ // frame should contain type, length, pushId
+ assert size <= 3 * VariableLengthEncoder.MAX_INTEGER_LENGTH;
+ ByteBuffer buffer = ByteBuffer.allocate((int) size);
+ cancelPush.writeFrame(buffer);
+ controlStreamPair.localWriter().scheduleForWriting(buffer, false);
+ } catch (IOException io) {
+ debug.log("Failed to cancel pushId: " + pushId);
+ }
+ }
+
+ /**
+ * Checks whether the given pushId exceed the maximum pushId allowed
+ * to the peer, and if so, closes the connection.
+ *
+ * @param pushId the pushId
+ * @return an {@code IOException} that can be used to complete a completable
+ * future if the maximum pushId is exceeded, {@code null}
+ * otherwise
+ */
+ IOException checkMaxPushId(long pushId) {
+ return checkMaxPushId(pushId, maxPushIdSent.get());
+ }
+
+ /**
+ * Checks whether the given pushId exceed the maximum pushId allowed
+ * to the peer, and if so, closes the connection.
+ *
+ * @param pushId the pushId
+ * @return an {@code IOException} that can be used to complete a completable
+ * future if the maximum pushId is exceeded, {@code null}
+ * otherwise
+ */
+ private IOException checkMaxPushId(long pushId, long max) {
+ if (pushId >= max) {
+ var io = new ProtocolException("Max pushId exceeded (%s >= %s)".formatted(pushId, max));
+ connectionError(io, Http3Error.H3_ID_ERROR);
+ return io;
+ }
+ return null;
+ }
+
+ /**
+ * {@return the minimum pushId that can be accepted from the peer}
+ * Any pushId strictly less than this value must be ignored.
+ *
+ * @apiNote The minimum pushId represents the smallest pushId that
+ * was recorded in our history. For smaller pushId, no history has
+ * been kept, due to history size constraints. Any pushId strictly
+ * less than this value must be ignored.
+ */
+ public long getMinPushId() {
+ return pushManager.getMinPushId();
+ }
+
+ private static final VarHandle CLOSED_STATE;
+ static {
+ try {
+ CLOSED_STATE = MethodHandles.lookup().findVarHandle(Http3Connection.class, "closedState", int.class);
+ } catch (Exception x) {
+ throw new ExceptionInInitializerError(x);
+ }
+ }
+}
diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http3ConnectionPool.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http3ConnectionPool.java
new file mode 100644
index 00000000000..eaacd8213cb
--- /dev/null
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http3ConnectionPool.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (c) 2025, 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. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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.internal.net.http;
+
+import java.net.http.HttpOption.Http3DiscoveryMode;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+
+import jdk.internal.net.http.common.Logger;
+
+import static java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC;
+import static java.net.http.HttpOption.Http3DiscoveryMode.ANY;
+import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY;
+
+/**
+ * This class encapsulate the HTTP/3 connection pool managed
+ * by an instance of {@link Http3ClientImpl}.
+ */
+class Http3ConnectionPool {
+ /* Map key is "scheme:host:port" */
+ private final Map advertised = new ConcurrentHashMap<>();
+ /* Map key is "scheme:host:port" */
+ private final Map unadvertised = new ConcurrentHashMap<>();
+
+ private final Logger debug;
+ Http3ConnectionPool(Logger logger) {
+ this.debug = Objects.requireNonNull(logger);
+ }
+
+ // https::
+ String connectionKey(HttpRequestImpl request) {
+ var uri = request.uri();
+ var scheme = uri.getScheme().toLowerCase(Locale.ROOT);
+ var host = uri.getHost();
+ var port = uri.getPort();
+ assert scheme.equals("https");
+ if (port < 0) port = 443; // https
+ return String.format("%s:%s:%d", scheme, host, port);
+ }
+
+ private Http3Connection lookupUnadvertised(String key, Http3DiscoveryMode discoveryMode) {
+ var unadvertisedConn = unadvertised.get(key);
+ if (unadvertisedConn == null) return null;
+ if (discoveryMode == ANY) return unadvertisedConn;
+ if (discoveryMode == ALT_SVC) return null;
+
+ assert discoveryMode == HTTP_3_URI_ONLY : String.valueOf(discoveryMode);
+
+ // Double check that if there is an alt service, it has same origin.
+ final var altService = Optional.ofNullable(unadvertisedConn)
+ .map(Http3Connection::connection)
+ .flatMap(HttpQuicConnection::getSourceAltService)
+ .orElse(null);
+
+ if (altService == null || altService.originHasSameAuthority()) {
+ return unadvertisedConn;
+ }
+
+ // We should never come here.
+ assert false : "unadvertised connection with different origin: %s -> %s"
+ .formatted(key, altService);
+ return null;
+ }
+
+ Http3Connection lookupFor(HttpRequestImpl request) {
+ var discoveryMode = request.http3Discovery();
+ var key = connectionKey(request);
+
+ Http3Connection unadvertisedConn = null;
+ // If not ALT_SVC, we can use unadvertised connections
+ if (discoveryMode != ALT_SVC) {
+ unadvertisedConn = lookupUnadvertised(key, discoveryMode);
+ if (unadvertisedConn != null && discoveryMode == HTTP_3_URI_ONLY) {
+ if (debug.on()) {
+ debug.log("Direct HTTP/3 connection found for %s in connection pool %s",
+ discoveryMode, unadvertisedConn.connection().label());
+ }
+ return unadvertisedConn;
+ }
+ }
+
+ // Then see if we have a connection which was advertised.
+ var advertisedConn = advertised.get(key);
+ // We can use it for HTTP3_URI_ONLY too if it has same origin
+ if (advertisedConn != null) {
+ final var altService = advertisedConn.connection()
+ .getSourceAltService().orElse(null);
+ assert altService != null && altService.wasAdvertised();
+ switch (discoveryMode) {
+ case ANY -> {
+ return advertisedConn;
+ }
+ case ALT_SVC -> {
+ if (debug.on()) {
+ debug.log("HTTP/3 connection found for %s in connection pool %s",
+ discoveryMode, advertisedConn.connection().label());
+ }
+ return advertisedConn;
+ }
+ case HTTP_3_URI_ONLY -> {
+ if (altService != null && altService.originHasSameAuthority()) {
+ if (debug.on()) {
+ debug.log("Same authority HTTP/3 connection found for %s in connection pool %s",
+ discoveryMode, advertisedConn.connection().label());
+ }
+ return advertisedConn;
+ }
+ }
+ }
+ }
+
+ if (unadvertisedConn != null) {
+ assert discoveryMode != ALT_SVC;
+ if (debug.on()) {
+ debug.log("Direct HTTP/3 connection found for %s in connection pool %s",
+ discoveryMode, unadvertisedConn.connection().label());
+ }
+ return unadvertisedConn;
+ }
+
+ // do not log here: this produces confusing logs as this method
+ // can be called several times when trying to establish a
+ // connection, when no connection is found in the pool
+ return null;
+ }
+
+ Http3Connection putIfAbsent(String key, Http3Connection c) {
+ Objects.requireNonNull(key);
+ Objects.requireNonNull(c);
+ assert key.equals(c.key());
+ var altService = c.connection().getSourceAltService().orElse(null);
+ if (altService != null && altService.wasAdvertised()) {
+ return advertised.putIfAbsent(key, c);
+ }
+ assert altService == null || altService.originHasSameAuthority();
+ return unadvertised.putIfAbsent(key, c);
+ }
+
+ Http3Connection put(String key, Http3Connection c) {
+ Objects.requireNonNull(key);
+ Objects.requireNonNull(c);
+ assert key.equals(c.key()) : "key mismatch %s -> %s"
+ .formatted(key, c.key());
+ var altService = c.connection().getSourceAltService().orElse(null);
+ if (altService != null && altService.wasAdvertised()) {
+ return advertised.put(key, c);
+ }
+ assert altService == null || altService.originHasSameAuthority();
+ return unadvertised.put(key, c);
+ }
+
+ boolean remove(String key, Http3Connection c) {
+ Objects.requireNonNull(key);
+ Objects.requireNonNull(c);
+ assert key.equals(c.key()) : "key mismatch %s -> %s"
+ .formatted(key, c.key());
+
+ var altService = c.connection().getSourceAltService().orElse(null);
+ if (altService != null && altService.wasAdvertised()) {
+ boolean remUndavertised = unadvertised.remove(key, c);
+ assert !remUndavertised
+ : "advertised connection found in unadvertised pool for " + key;
+ return advertised.remove(key, c);
+ }
+
+ assert altService == null || altService.originHasSameAuthority();
+ return unadvertised.remove(key, c);
+ }
+
+ void clear() {
+ advertised.clear();
+ unadvertised.clear();
+ }
+
+ java.util.stream.Stream values() {
+ return java.util.stream.Stream.concat(
+ advertised.values().stream(),
+ unadvertised.values().stream());
+ }
+
+}
+
diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http3ExchangeImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http3ExchangeImpl.java
new file mode 100644
index 00000000000..ff1e024673c
--- /dev/null
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http3ExchangeImpl.java
@@ -0,0 +1,1795 @@
+/*
+ * Copyright (c) 2022, 2025, 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. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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.internal.net.http;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.VarHandle;
+import java.net.ProtocolException;
+import java.net.http.HttpClient.Version;
+import java.net.http.HttpHeaders;
+import java.net.http.HttpRequest.BodyPublisher;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandler;
+import java.net.http.HttpResponse.BodySubscriber;
+import java.net.http.HttpResponse.ResponseInfo;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.ConcurrentHashMap;
+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.AtomicLong;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.function.BiPredicate;
+
+import jdk.internal.net.http.PushGroup.Acceptor;
+import jdk.internal.net.http.common.HttpBodySubscriberWrapper;
+import jdk.internal.net.http.common.HttpHeadersBuilder;
+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.SequentialScheduler;
+import jdk.internal.net.http.common.SubscriptionBase;
+import jdk.internal.net.http.common.Utils;
+import jdk.internal.net.http.common.ValidatingHeadersConsumer;
+import jdk.internal.net.http.http3.ConnectionSettings;
+import jdk.internal.net.http.http3.Http3Error;
+import jdk.internal.net.http.http3.frames.DataFrame;
+import jdk.internal.net.http.http3.frames.FramesDecoder;
+import jdk.internal.net.http.http3.frames.HeadersFrame;
+import jdk.internal.net.http.http3.frames.PushPromiseFrame;
+import jdk.internal.net.http.qpack.Decoder;
+import jdk.internal.net.http.qpack.DecodingCallback;
+import jdk.internal.net.http.qpack.Encoder;
+import jdk.internal.net.http.qpack.QPackException;
+import jdk.internal.net.http.qpack.readers.HeaderFrameReader;
+import jdk.internal.net.http.qpack.writers.HeaderFrameWriter;
+import jdk.internal.net.http.quic.streams.QuicBidiStream;
+import jdk.internal.net.http.quic.streams.QuicStreamReader;
+import jdk.internal.net.http.quic.streams.QuicStreamWriter;
+import static jdk.internal.net.http.http3.ConnectionSettings.UNLIMITED_MAX_FIELD_SECTION_SIZE;
+
+/**
+ * This class represents an HTTP/3 Request/Response stream.
+ */
+final class Http3ExchangeImpl extends Http3Stream {
+
+ private static final String COOKIE_HEADER = "Cookie";
+ private final Logger debug = Utils.getDebugLogger(this::dbgTag);
+ private final Http3Connection connection;
+ private final HttpRequestImpl request;
+ private final BodyPublisher requestPublisher;
+ private final HttpHeadersBuilder responseHeadersBuilder;
+ private final HeadersConsumer rspHeadersConsumer;
+ private final HttpHeaders requestPseudoHeaders;
+ private final HeaderFrameReader headerFrameReader;
+ private final HeaderFrameWriter headerFrameWriter;
+ private final Decoder qpackDecoder;
+ private final Encoder qpackEncoder;
+ private final AtomicReference errorRef;
+ private final CompletableFuture requestBodyCF;
+
+ private final FramesDecoder framesDecoder =
+ new FramesDecoder(this::dbgTag, FramesDecoder::isAllowedOnRequestStream);
+ private final SequentialScheduler readScheduler =
+ SequentialScheduler.lockingScheduler(this::processQuicData);
+ private final SequentialScheduler writeScheduler =
+ SequentialScheduler.lockingScheduler(this::sendQuicData);
+ private final List> response_cfs = new ArrayList<>(5);
+ private final ReentrantLock stateLock = new ReentrantLock();
+ private final ReentrantLock response_cfs_lock = new ReentrantLock();
+ private final H3FrameOrderVerifier frameOrderVerifier = H3FrameOrderVerifier.newForRequestResponseStream();
+
+
+ final SubscriptionBase userSubscription =
+ new SubscriptionBase(readScheduler, this::cancel, this::onSubscriptionError);
+
+ private final QuicBidiStream stream;
+ private final QuicStreamReader reader;
+ private final QuicStreamWriter writer;
+ volatile boolean closed;
+ volatile RequestSubscriber requestSubscriber;
+ volatile HttpResponse.BodySubscriber pendingResponseSubscriber;
+ volatile HttpResponse.BodySubscriber responseSubscriber;
+ volatile CompletableFuture responseBodyCF;
+ volatile boolean requestSent;
+ volatile boolean responseReceived;
+ volatile long requestContentLen;
+ volatile int responseCode;
+ volatile Response response;
+ volatile boolean stopRequested;
+ volatile boolean deRegistered;
+ private String dbgTag = null;
+ private final AtomicLong sentQuicBytes = new AtomicLong();
+
+ Http3ExchangeImpl(final Http3Connection connection, final Exchange exchange,
+ final QuicBidiStream stream) {
+ super(exchange);
+ this.errorRef = new AtomicReference<>();
+ this.requestBodyCF = new MinimalFuture<>();
+ this.connection = connection;
+ this.request = exchange.request();
+ this.requestPublisher = request.requestPublisher; // may be null
+ this.responseHeadersBuilder = new HttpHeadersBuilder();
+ this.rspHeadersConsumer = new HeadersConsumer(ValidatingHeadersConsumer.Context.RESPONSE);
+ this.qpackDecoder = connection.qpackDecoder();
+ this.qpackEncoder = connection.qpackEncoder();
+ this.headerFrameReader = qpackDecoder.newHeaderFrameReader(rspHeadersConsumer);
+ this.headerFrameWriter = qpackEncoder.newHeaderFrameWriter();
+ this.requestPseudoHeaders = Utils.createPseudoHeaders(request);
+ this.stream = stream;
+ this.reader = stream.connectReader(readScheduler);
+ this.writer = stream.connectWriter(writeScheduler);
+ if (debug.on()) debug.log("Http3ExchangeImpl created");
+ }
+
+ public void start() {
+ if (exchange.pushGroup != null) {
+ connection.checkSendMaxPushId();
+ }
+ if (Log.http3()) {
+ Log.logHttp3("Starting HTTP/3 exchange for {0}/streamId={1} ({2} #{3})",
+ connection.quicConnectionTag(), Long.toString(stream.streamId()),
+ request, Long.toString(exchange.multi.id));
+ }
+ this.reader.start();
+ }
+
+ boolean acceptPushPromise() {
+ return exchange.pushGroup != null;
+ }
+
+ String dbgTag() {
+ if (dbgTag != null) return dbgTag;
+ long streamId = streamId();
+ String sid = streamId == -1 ? "?" : String.valueOf(streamId);
+ String ctag = connection == null ? null : connection.dbgTag();
+ String tag = "Http3ExchangeImpl(" + ctag + ", streamId=" + sid + ")";
+ if (streamId == -1) return tag;
+ return dbgTag = tag;
+ }
+
+ @Override
+ long streamId() {
+ var stream = this.stream;
+ return stream == null ? -1 : stream.streamId();
+ }
+
+ Http3Connection http3Connection() {
+ return connection;
+ }
+
+ void recordError(Throwable closeCause) {
+ errorRef.compareAndSet(null, closeCause);
+ }
+
+ private sealed class HeadersConsumer extends StreamHeadersConsumer permits PushHeadersConsumer {
+
+ private HeadersConsumer(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected HeaderFrameReader headerFrameReader() {
+ return headerFrameReader;
+ }
+
+ @Override
+ protected HttpHeadersBuilder headersBuilder() {
+ return responseHeadersBuilder;
+ }
+
+ @Override
+ protected final Decoder qpackDecoder() {
+ return qpackDecoder;
+ }
+
+ void resetDone() {
+ if (debug.on()) {
+ debug.log("Response builder cleared, ready to receive new headers.");
+ }
+ }
+
+
+ @Override
+ String headerFieldType() {
+ return "RESPONSE HEADER FIELD";
+ }
+
+ @Override
+ protected String formatMessage(String message, String header) {
+ // Malformed requests or responses that are detected MUST be
+ // treated as a stream error of type H3_MESSAGE_ERROR.
+ return "malformed response: " + super.formatMessage(message, header);
+ }
+
+ @Override
+ protected void headersCompleted() {
+ handleResponse();
+ }
+
+ @Override
+ public final long streamId() {
+ return stream.streamId();
+ }
+
+ }
+
+ private final class PushHeadersConsumer extends HeadersConsumer {
+ volatile PushPromiseState state;
+
+ private PushHeadersConsumer() {
+ super(Context.REQUEST);
+ }
+
+ @Override
+ protected HttpHeadersBuilder headersBuilder() {
+ return state.headersBuilder();
+ }
+
+ @Override
+ protected HeaderFrameReader headerFrameReader() {
+ return state.reader();
+ }
+
+ @Override
+ String headerFieldType() {
+ return "PUSH REQUEST HEADER FIELD";
+ }
+
+ void resetDone() {
+ if (debug.on()) {
+ debug.log("Push request builder cleared.");
+ }
+ }
+
+ @Override
+ protected String formatMessage(String message, String header) {
+ // Malformed requests or responses that are detected MUST be
+ // treated as a stream error of type H3_MESSAGE_ERROR.
+ return "malformed push request: " + super.formatMessage(message, header);
+ }
+
+ @Override
+ protected void headersCompleted() {
+ try {
+ if (exchange.pushGroup == null) {
+ long pushId = state.frame().getPushId();
+ connection.noPushHandlerFor(pushId);
+ reset();
+ } else {
+ handlePromise(this);
+ }
+ } catch (IOException io) {
+ cancelPushPromise(state, io);
+ }
+ }
+
+ public void setState(PushPromiseState state) {
+ this.state = state;
+ }
+ }
+
+ // TODO: this is also defined on Stream
+ //
+ private static boolean hasProxyAuthorization(HttpHeaders headers) {
+ return headers.firstValue("proxy-authorization")
+ .isPresent();
+ }
+
+ // TODO: this is also defined on Stream
+ //
+ // Determines whether we need to build a new HttpHeader object.
+ //
+ // Ideally we should pass the filter to OutgoingHeaders refactor the
+ // code that creates the HeaderFrame to honor the filter.
+ // We're not there yet - so depending on the filter we need to
+ // apply and the content of the header we will try to determine
+ // whether anything might need to be filtered.
+ // If nothing needs filtering then we can just use the
+ // original headers.
+ private static boolean needsFiltering(HttpHeaders headers,
+ BiPredicate filter) {
+ if (filter == Utils.PROXY_TUNNEL_FILTER || filter == Utils.PROXY_FILTER) {
+ // we're either connecting or proxying
+ // slight optimization: we only need to filter out
+ // disabled schemes, so if there are none just
+ // pass through.
+ return Utils.proxyHasDisabledSchemes(filter == Utils.PROXY_TUNNEL_FILTER)
+ && hasProxyAuthorization(headers);
+ } else {
+ // we're talking to a server, either directly or through
+ // a tunnel.
+ // Slight optimization: we only need to filter out
+ // proxy authorization headers, so if there are none just
+ // pass through.
+ return hasProxyAuthorization(headers);
+ }
+ }
+
+ // TODO: this is also defined on Stream
+ //
+ private HttpHeaders filterHeaders(HttpHeaders headers) {
+ HttpConnection conn = connection();
+ BiPredicate filter = conn.headerFilter(request);
+ if (needsFiltering(headers, filter)) {
+ return HttpHeaders.of(headers.map(), filter);
+ }
+ return headers;
+ }
+
+ @Override
+ HttpQuicConnection connection() {
+ return connection.connection();
+ }
+
+ @Override
+ CompletableFuture> sendHeadersAsync() {
+ final MinimalFuture completable = MinimalFuture.completedFuture(null);
+ return completable.thenApply(_ -> this.sendHeaders());
+ }
+
+ private Http3ExchangeImpl sendHeaders() {
+ assert stream != null;
+ assert writer != null;
+
+ if (debug.on()) debug.log("H3 sendHeaders");
+ if (Log.requests()) {
+ Log.logRequest(request.toString());
+ }
+ if (requestPublisher != null) {
+ requestContentLen = requestPublisher.contentLength();
+ } else {
+ requestContentLen = 0;
+ }
+
+ Throwable t = errorRef.get();
+ if (t != null) {
+ if (debug.on()) debug.log("H3 stream already cancelled, headers not sent: %s", (Object) t);
+ if (t instanceof CompletionException ce) throw ce;
+ throw new CompletionException(t);
+ }
+
+ HttpHeadersBuilder h = request.getSystemHeadersBuilder();
+ if (requestContentLen > 0) {
+ h.setHeader("content-length", Long.toString(requestContentLen));
+ }
+ HttpHeaders sysh = filterHeaders(h.build());
+ HttpHeaders userh = filterHeaders(request.getUserHeaders());
+ // Filter context restricted from userHeaders
+ userh = HttpHeaders.of(userh.map(), Utils.ACCEPT_ALL);
+ Utils.setUserAuthFlags(request, userh);
+
+ // Don't override Cookie values that have been set by the CookieHandler.
+ final HttpHeaders uh = userh;
+ BiPredicate overrides =
+ (k, v) -> COOKIE_HEADER.equalsIgnoreCase(k)
+ || uh.firstValue(k).isEmpty();
+
+ // Filter any headers from systemHeaders that are set in userHeaders
+ // except for "Cookie:" - user cookies will be appended to system
+ // cookies
+ sysh = HttpHeaders.of(sysh.map(), overrides);
+
+ if (Log.headers() || debug.on()) {
+ StringBuilder sb = new StringBuilder("H3 HEADERS FRAME (stream=");
+ sb.append(streamId()).append(")\n");
+ Log.dumpHeaders(sb, " ", requestPseudoHeaders);
+ Log.dumpHeaders(sb, " ", sysh);
+ Log.dumpHeaders(sb, " ", userh);
+ if (Log.headers()) {
+ Log.logHeaders(sb.toString());
+ } else if (debug.on()) {
+ debug.log(sb);
+ }
+ }
+
+ final Optional peerSettings = connection.getPeerSettings();
+ // It's possible that the peer settings hasn't yet arrived, in which case we use the
+ // default of "unlimited" header size limit and proceed with sending the request. As per
+ // RFC-9114, section 7.2.4.2, this is allowed: All settings begin at an initial value. Each
+ // endpoint SHOULD use these initial values to send messages before the peer's SETTINGS frame
+ // has arrived, as packets carrying the settings can be lost or delayed.
+ // When the SETTINGS frame arrives, any settings are changed to their new values. This
+ // removes the need to wait for the SETTINGS frame before sending messages.
+ final long headerSizeLimit = peerSettings.isEmpty() ? UNLIMITED_MAX_FIELD_SECTION_SIZE
+ : peerSettings.get().maxFieldSectionSize();
+ if (headerSizeLimit != UNLIMITED_MAX_FIELD_SECTION_SIZE) {
+ // specific limit has been set on the header size for this connection.
+ // we compute the header size and ensure that it doesn't exceed that limit
+ final long computedHeaderSize = computeHeaderSize(requestPseudoHeaders, sysh, userh);
+ if (computedHeaderSize > headerSizeLimit) {
+ // RFC-9114, section 4.2.2: An implementation that has received this parameter
+ // SHOULD NOT send an HTTP message header that exceeds the indicated size.
+ // we fail the request.
+ throw new CompletionException(new ProtocolException("Request headers size" +
+ " exceeds limit set by peer"));
+ }
+ }
+ List buffers = qpackEncoder.encodeHeaders(headerFrameWriter, streamId(),
+ 1024, requestPseudoHeaders, sysh, userh);
+ HeadersFrame headersFrame = new HeadersFrame(Utils.remaining(buffers));
+ ByteBuffer buffer = ByteBuffer.allocate(headersFrame.headersSize());
+ headersFrame.writeHeaders(buffer);
+ buffer.flip();
+ long sentBytes = 0;
+ try {
+ boolean hasNoBody = requestContentLen == 0;
+ int last = buffers.size() - 1;
+ int toSend = buffer.remaining();
+ if (last < 0) {
+ writer.scheduleForWriting(buffer, hasNoBody);
+ } else {
+ writer.queueForWriting(buffer);
+ }
+ sentBytes += toSend;
+ for (int i = 0; i <= last; i++) {
+ var nextBuffer = buffers.get(i);
+ toSend = nextBuffer.remaining();
+ if (i == last) {
+ writer.scheduleForWriting(nextBuffer, hasNoBody);
+ } else {
+ writer.queueForWriting(nextBuffer);
+ }
+ sentBytes += toSend;
+ }
+ } catch (QPackException qe) {
+ if (qe.isConnectionError()) {
+ // close the connection
+ connection.close(qe.http3Error(), "QPack error", qe.getCause());
+ }
+ // fail the request
+ throw new CompletionException(qe.getCause());
+ } catch (IOException io) {
+ throw new CompletionException(io);
+ } finally {
+ if (sentBytes != 0) sentQuicBytes.addAndGet(sentBytes);
+ }
+ return this;
+ }
+
+ private static long computeHeaderSize(final HttpHeaders... headers) {
+ // RFC-9114, section 4.2.2 states: The size of a field list is calculated based on
+ // the uncompressed size of fields, including the length of the name and value in bytes
+ // plus an overhead of 32 bytes for each field.
+ final int OVERHEAD_BYTES_PER_FIELD = 32;
+ long computedHeaderSize = 0;
+ for (final HttpHeaders h : headers) {
+ for (final Map.Entry> entry : h.map().entrySet()) {
+ try {
+ computedHeaderSize = Math.addExact(computedHeaderSize,
+ entry.getKey().getBytes(StandardCharsets.US_ASCII).length);
+ for (final String v : entry.getValue()) {
+ computedHeaderSize = Math.addExact(computedHeaderSize,
+ v.getBytes(StandardCharsets.US_ASCII).length);
+ }
+ computedHeaderSize = Math.addExact(computedHeaderSize, OVERHEAD_BYTES_PER_FIELD);
+ } catch (ArithmeticException ae) {
+ // overflow, no point trying to compute further, return MAX_VALUE
+ return Long.MAX_VALUE;
+ }
+ }
+ }
+ return computedHeaderSize;
+ }
+
+
+ @Override
+ CompletableFuture> sendBodyAsync() {
+ return sendBodyImpl().thenApply((e) -> this);
+ }
+
+ CompletableFuture sendBodyImpl() {
+ requestBodyCF.whenComplete((v, t) -> requestSent());
+ try {
+ if (debug.on()) debug.log("H3 sendBodyImpl");
+ if (requestPublisher != null && requestContentLen != 0) {
+ final RequestSubscriber subscriber = new RequestSubscriber(requestContentLen);
+ requestPublisher.subscribe(requestSubscriber = subscriber);
+ } else {
+ // there is no request body, therefore the request is complete,
+ // END_STREAM has already sent with outgoing headers
+ requestBodyCF.complete(null);
+ }
+ } catch (Throwable t) {
+ cancelImpl(t, Http3Error.H3_REQUEST_CANCELLED);
+ requestBodyCF.completeExceptionally(t);
+ }
+ return requestBodyCF;
+ }
+
+ // The Http3StreamResponseSubscriber is registered with the HttpClient
+ // to ensure that it gets completed if the SelectorManager aborts due
+ // to unexpected exceptions.
+ private void registerResponseSubscriber(Http3StreamResponseSubscriber> subscriber) {
+ if (client().registerSubscriber(subscriber)) {
+ if (debug.on()) {
+ debug.log("Reference response body for h3 stream: " + streamId());
+ }
+ client().h3StreamReference();
+ }
+ }
+
+ private void unregisterResponseSubscriber(Http3StreamResponseSubscriber> subscriber) {
+ if (client().unregisterSubscriber(subscriber)) {
+ if (debug.on()) {
+ debug.log("Unreference response body for h3 stream: " + streamId());
+ }
+ client().h3StreamUnreference();
+ }
+ }
+
+ final class Http3StreamResponseSubscriber extends HttpBodySubscriberWrapper {
+ Http3StreamResponseSubscriber(BodySubscriber subscriber) {
+ super(subscriber);
+ }
+
+ @Override
+ protected void unregister() {
+ unregisterResponseSubscriber(this);
+ }
+
+ @Override
+ protected void register() {
+ registerResponseSubscriber(this);
+ }
+
+ @Override
+ protected void logComplete(Throwable error) {
+ if (error == null) {
+ if (Log.requests()) {
+ Log.logResponse(() -> "HTTP/3 body successfully completed for: " + request
+ + " #" + exchange.multi.id);
+ }
+ } else {
+ if (Log.requests()) {
+ Log.logResponse(() -> "HTTP/3 body exceptionally completed for: "
+ + request + " (" + error + ")"
+ + " #" + exchange.multi.id);
+ }
+ }
+ }
+ }
+
+
+ @Override
+ Http3StreamResponseSubscriber createResponseSubscriber(BodyHandler handler,
+ ResponseInfo response) {
+ if (debug.on()) debug.log("Creating body subscriber");
+ Http3StreamResponseSubscriber subscriber =
+ new Http3StreamResponseSubscriber<>(handler.apply(response));
+ return subscriber;
+ }
+
+ @Override
+ CompletableFuture readBodyAsync(BodyHandler handler,
+ boolean returnConnectionToPool,
+ Executor executor) {
+ try {
+ if (Log.trace()) {
+ Log.logTrace("Reading body on stream {0}", streamId());
+ }
+ if (debug.on()) debug.log("Getting BodySubscriber for: " + response);
+ Http3StreamResponseSubscriber bodySubscriber =
+ createResponseSubscriber(handler, new ResponseInfoImpl(response));
+ CompletableFuture cf = receiveResponseBody(bodySubscriber, executor);
+
+ PushGroup> pg = exchange.getPushGroup();
+ if (pg != null) {
+ // if an error occurs make sure it is recorded in the PushGroup
+ cf = cf.whenComplete((t, e) -> pg.pushError(e));
+ }
+ var bodyCF = cf;
+ return bodyCF;
+ } catch (Throwable t) {
+ // may be thrown by handler.apply
+ // TODO: Is this the right error code?
+ cancelImpl(t, Http3Error.H3_REQUEST_CANCELLED);
+ return MinimalFuture.failedFuture(t);
+ }
+ }
+
+ @Override
+ CompletableFuture ignoreBody() {
+ try {
+ if (debug.on()) debug.log("Ignoring body");
+ reader.stream().requestStopSending(Http3Error.H3_REQUEST_CANCELLED.code());
+ return MinimalFuture.completedFuture(null);
+ } catch (Throwable e) {
+ if (Log.trace()) {
+ Log.logTrace("Error requesting stop sending for stream {0}: {1}",
+ streamId(), e.toString());
+ }
+ return MinimalFuture.failedFuture(e);
+ }
+ }
+
+ @Override
+ void cancel() {
+ if (debug.on()) debug.log("cancel");
+ var stream = this.stream;
+ if ((stream == null)) {
+ cancel(new IOException("Stream cancelled before streamid assigned"));
+ } else {
+ cancel(new IOException("Stream " + stream.streamId() + " cancelled"));
+ }
+ }
+
+ @Override
+ void cancel(IOException cause) {
+ cancelImpl(cause, Http3Error.H3_REQUEST_CANCELLED);
+ }
+
+ @Override
+ void onProtocolError(IOException cause) {
+ final long streamId = stream.streamId();
+ if (debug.on()) {
+ debug.log("cancelling exchange on stream %d due to protocol error: %s", streamId, cause.getMessage());
+ }
+ Log.logError("cancelling exchange on stream {0} due to protocol error: {1}\n", streamId, cause);
+ cancelImpl(cause, Http3Error.H3_GENERAL_PROTOCOL_ERROR);
+ }
+
+ @Override
+ void released() {
+ long streamid = streamId();
+ if (debug.on()) debug.log("Released stream %d", streamid);
+ // remove this stream from the Http2Connection map.
+ connection.onExchangeClose(this, streamid);
+ }
+
+ @Override
+ void completed() {
+ }
+
+ @Override
+ boolean isCanceled() {
+ return errorRef.get() != null;
+ }
+
+ @Override
+ Throwable getCancelCause() {
+ return errorRef.get();
+ }
+
+ @Override
+ void cancelImpl(Throwable e, Http3Error error) {
+ try {
+ var streamid = streamId();
+ if (errorRef.compareAndSet(null, e)) {
+ if (debug.on()) {
+ if (streamid == -1) debug.log("cancelling stream", e);
+ else debug.log("cancelling stream " + streamid + ":", e);
+ }
+ if (Log.trace()) {
+ if (streamid == -1) Log.logTrace("cancelling stream: {0}\n", e);
+ else Log.logTrace("cancelling stream {0}: {1}\n", streamid, e);
+ }
+ } else {
+ if (debug.on()) {
+ if (streamid == -1) debug.log("cancelling stream: %s", (Object) e);
+ else debug.log("cancelling stream %s: %s", streamid, e);
+ }
+ }
+ var firstError = errorRef.get();
+ completeResponseExceptionally(firstError);
+ if (!requestBodyCF.isDone()) {
+ // complete requestBodyCF before cancelling subscription
+ requestBodyCF.completeExceptionally(firstError); // we may be sending the body...
+ var requestSubscriber = this.requestSubscriber;
+ if (requestSubscriber != null) {
+ cancel(requestSubscriber.subscription.get());
+ }
+ }
+ var responseBodyCF = this.responseBodyCF;
+ if (responseBodyCF != null) {
+ responseBodyCF.completeExceptionally(firstError);
+ }
+ // will send a RST_STREAM frame
+ var stream = this.stream;
+ if (connection.isOpen()) {
+ if (stream != null && stream.sendingState().isSending()) {
+ // no use reset if already closed.
+ var cause = Utils.getCompletionCause(firstError);
+ if (!(cause instanceof EOFException)) {
+ if (debug.on())
+ debug.log("sending reset %s", error);
+ stream.reset(error.code());
+ }
+ }
+ if (stream != null) {
+ if (debug.on())
+ debug.log("request stop sending");
+ stream.requestStopSending(error.code());
+ }
+ }
+ } catch (Throwable ex) {
+ errorRef.compareAndSet(null, ex);
+ if (debug.on())
+ debug.log("failed cancelling request: ", ex);
+ Log.logError(ex);
+ } finally {
+ close();
+ }
+ }
+
+ // cancel subscription and ignore errors in order to continue with
+ // the cancel/close sequence.
+ private void cancel(Subscription subscription) {
+ if (subscription == null) return;
+ try { subscription.cancel(); }
+ catch (Throwable t) {
+ debug.log("Unexpected exception thrown by Subscription::cancel", t);
+ if (Log.errors()) {
+ Log.logError("Unexpected exception thrown by Subscription::cancel: " + t);
+ Log.logError(t);
+ }
+ }
+ }
+
+ @Override
+ CompletableFuture