8349910: Implement JEP 517: HTTP/3 for the HTTP Client API

Co-authored-by: Aleksei Efimov <aefimov@openjdk.org>
Co-authored-by: Bradford Wetmore <wetmore@openjdk.org>
Co-authored-by: Daniel Jeliński <djelinski@openjdk.org>
Co-authored-by: Darragh Clarke <dclarke@openjdk.org>
Co-authored-by: Jaikiran Pai <jpai@openjdk.org>
Co-authored-by: Michael McMahon <michaelm@openjdk.org>
Co-authored-by: Volkan Yazici <vyazici@openjdk.org>
Co-authored-by: Conor Cleary <conor.cleary@oracle.com>
Co-authored-by: Patrick Concannon <patrick.concannon@oracle.com>
Co-authored-by: Rahul Yadav <rahul.r.yadav@oracle.com>
Co-authored-by: Daniel Fuchs <dfuchs@openjdk.org>
Reviewed-by: djelinski, jpai, aefimov, abarashev, michaelm
This commit is contained in:
Daniel Fuchs 2025-09-22 10:12:12 +00:00
parent 433d2ec534
commit e8db14f584
473 changed files with 104314 additions and 2646 deletions

View File

@ -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);
}
}

View File

@ -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();
}

View File

@ -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
* <p>
* 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.
* <p>
* 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);
}
}
}

View File

@ -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<QuicVersion> 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.
* <p>
* 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.
* <p>
* 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}.
* <p>
* 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}.
* <p>
* 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.
* <p>
* 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}.
* <p>
* 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.
* <p>
* It is recommended to do the encryption in place by using slices of a bigger
* buffer as the input and output buffer:
* <pre>
* +--------+-------------------+
* input: | header | plaintext payload |
* +--------+-------------------+----------+
* output: | encrypted payload | AEAD tag |
* +-------------------+----------+
* </pre>
*
* @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<ByteBuffer> 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.
* <p>
* 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.
* <p>
* The decrypted payload bytes are written
* to the output buffer.
* <p>
* It is recommended to do the decryption in place by using slices of a bigger
* buffer as the input and output buffer:
* <pre>
* +--------+-------------------+----------+
* input: | header | encrypted payload | AEAD tag |
* +--------+-------------------+----------+
* output: | decrypted payload |
* +-------------------+
* </pre>
*
* @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}.
* <P>
* {@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.
* <P>
* A call to this method will return each outstanding task
* exactly once.
* <P>
* 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}.
* <p> 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);
}

View File

@ -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
* <a href="https://www.rfc-editor.org/rfc/rfc9000#name-error-codes">RFC 9000, section 20.1</a>.
*/
public enum QuicTransportErrors {
/**
* No error.
* <p>
* From <a href="https://www.rfc-editor.org/rfc/rfc9000#name-error-codes">
* RFC 9000, Section 20.1</a>:
* <blockquote><pre>{@code
* An endpoint uses this with CONNECTION_CLOSE to signal that
* the connection is being closed abruptly in the absence
* of any error.
* }</pre></blockquote>
*/
NO_ERROR(0x00),
/**
* Internal Error.
* <p>
* From <a href="https://www.rfc-editor.org/rfc/rfc9000#name-error-codes">
* RFC 9000, Section 20.1</a>:
* <blockquote><pre>{@code
* The endpoint encountered an internal error and cannot
* continue with the connection.
* }</pre></blockquote>
*/
INTERNAL_ERROR(0x01),
/**
* Connection refused error.
* <p>
* From <a href="https://www.rfc-editor.org/rfc/rfc9000#name-error-codes">
* RFC 9000, Section 20.1</a>:
* <blockquote><pre>{@code
* The server refused to accept a new connection.
* }</pre></blockquote>
*/
CONNECTION_REFUSED(0x02),
/**
* Flow control error.
* <p>
* From <a href="https://www.rfc-editor.org/rfc/rfc9000#name-error-codes">
* RFC 9000, Section 20.1</a>:
* <blockquote><pre>{@code
* An endpoint received more data than it permitted in its advertised data limits;
* see Section 4.
* }</pre></blockquote>
* @see<a href="https://www.rfc-editor.org/rfc/rfc9000#section-4>RFC 9000, Section 4</a>
*/
FLOW_CONTROL_ERROR(0x03),
/**
* Stream limit error.
* <p>
* From <a href="https://www.rfc-editor.org/rfc/rfc9000#name-error-codes">
* RFC 9000, Section 20.1</a>:
* <blockquote><pre>{@code
* An endpoint received a frame for a stream identifier that exceeded its advertised
* stream limit for the corresponding stream type.
* }</pre></blockquote>
*/
STREAM_LIMIT_ERROR(0x04),
/**
* Stream state error.
* <p>
* From <a href="https://www.rfc-editor.org/rfc/rfc9000#name-error-codes">
* RFC 9000, Section 20.1</a>:
* <blockquote><pre>{@code
* An endpoint received a frame for a stream that was not in a state that permitted
* that frame; see Section 3.
* }</pre></blockquote>
* @see <a href="https://www.rfc-editor.org/rfc/rfc9000#section-3>RFC 9000, Section 3</a>.
*/
STREAM_STATE_ERROR(0x05),
/**
* Final size error.
* <p>
* From <a href="https://www.rfc-editor.org/rfc/rfc9000#name-error-codes">
* RFC 9000, Section 20.1</a>:
* <blockquote><pre>{@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.
* }</pre></blockquote>
*/
FINAL_SIZE_ERROR(0x06),
/**
* Frame encoding error.
* <p>
* From <a href="https://www.rfc-editor.org/rfc/rfc9000#name-error-codes">
* RFC 9000, Section 20.1</a>:
* <blockquote><pre>{@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.
* }</pre></blockquote>
*/
FRAME_ENCODING_ERROR(0x07),
/**
* Transport parameter error.
* <p>
* From <a href="https://www.rfc-editor.org/rfc/rfc9000#name-error-codes">
* RFC 9000, Section 20.1</a>:
* <blockquote><pre>{@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.
* }</pre></blockquote>
*/
TRANSPORT_PARAMETER_ERROR(0x08),
/**
* Connection id limit error.
* <p>
* From <a href="https://www.rfc-editor.org/rfc/rfc9000#name-error-codes">
* RFC 9000, Section 20.1</a>:
* <blockquote><pre>{@code
* The number of connection IDs provided by the peer exceeds
* the advertised active_connection_id_limit.
* }</pre></blockquote>
*/
CONNECTION_ID_LIMIT_ERROR(0x09),
/**
* Protocol violiation error.
* <p>
* From <a href="https://www.rfc-editor.org/rfc/rfc9000#name-error-codes">
* RFC 9000, Section 20.1</a>:
* <blockquote><pre>{@code
* An endpoint detected an error with protocol compliance that
* was not covered by more specific error codes.
* }</pre></blockquote>
*/
PROTOCOL_VIOLATION(0x0a),
/**
* Invalid token error.
* <p>
* From <a href="https://www.rfc-editor.org/rfc/rfc9000#name-error-codes">
* RFC 9000, Section 20.1</a>:
* <blockquote><pre>{@code
* A server received a client Initial that contained an invalid Token field.
* }</pre></blockquote>
*/
INVALID_TOKEN(0x0b),
/**
* Application error.
* <p>
* From <a href="https://www.rfc-editor.org/rfc/rfc9000#name-error-codes">
* RFC 9000, Section 20.1</a>:
* <blockquote><pre>{@code
* The application or application protocol caused the connection to be closed.
* }</pre></blockquote>
*/
APPLICATION_ERROR(0x0c),
/**
* Crypto buffer exceeded error.
* <p>
* From <a href="https://www.rfc-editor.org/rfc/rfc9000#name-error-codes">
* RFC 9000, Section 20.1</a>:
* <blockquote><pre>{@code
* An endpoint has received more data in CRYPTO frames than it can buffer.
* }</pre></blockquote>
*/
CRYPTO_BUFFER_EXCEEDED(0x0d),
/**
* Key update error.
* <p>
* From <a href="https://www.rfc-editor.org/rfc/rfc9000#name-error-codes">
* RFC 9000, Section 20.1</a>:
* <blockquote><pre>{@code
* An endpoint detected errors in performing key updates; see Section 6 of [QUIC-TLS].
* }</pre></blockquote>
* @see <a href="https://www.rfc-editor.org/rfc/rfc9001#section-6">Section 6 of RFC 9001 [QUIC-TLS]</a>
*/
KEY_UPDATE_ERROR(0x0e),
/**
* AEAD limit reached error
* <p>
* From <a href="https://www.rfc-editor.org/rfc/rfc9000#name-error-codes">
* RFC 9000, Section 20.1</a>:
* <blockquote><pre>{@code
* An endpoint has reached the confidentiality or integrity limit
* for the AEAD algorithm used by the given connection.
* }</pre></blockquote>
*/
AEAD_LIMIT_REACHED(0x0f),
/**
* No viable path error.
* <p>
* From <a href="https://www.rfc-editor.org/rfc/rfc9000#name-error-codes">
* RFC 9000, Section 20.1</a>:
* <blockquote><pre>{@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.
* }</pre></blockquote>
*/
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.
* <p>
* From <a href="https://www.rfc-editor.org/rfc/rfc9000#name-error-codes">
* RFC 9000, Section 20.1</a>:
* <blockquote><pre>{@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].
* }</pre></blockquote>
* @see <a href="https://www.rfc-editor.org/rfc/rfc9001#section-6">Section 4.8 of RFC 9001 [QUIC-TLS]</a>
*/
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<QuicTransportErrors> ofCode(long code) {
return Stream.of(values()).filter(e -> e.isFor(code)).findAny();
}
public static String toString(long code) {
Optional<QuicTransportErrors> 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) + "]";
}
}

View File

@ -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.
*
* <p>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;
}
}

View File

@ -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;
}

View File

@ -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<QuicVersion> 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<QuicVersion> 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;
}
}

View File

@ -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,

View File

@ -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;

View File

@ -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.
*
* <p>
* 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 = "";

View File

@ -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

View File

@ -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.

View File

@ -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);

View File

@ -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(

View File

@ -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();

View File

@ -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();

View File

@ -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<String, Long> 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<String, Long> limits = new HashMap<>();
for (final String entry : propVal.split(",")) {
// each entry is of the form <cipher> <limit>
// 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);
}
}
}
}

View File

@ -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<HandshakeMemo> 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();
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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.
* <p>
* 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<Byte, KeySpace> 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<QuicVersion> 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<String> 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<QuicVersion> 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<QuicVersion> 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<ByteBuffer> 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();
}
}

View File

@ -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");
}
}
}
}

View File

@ -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.
* <p>
* 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<CryptoPrimitive> primitives,
String algorithm, AlgorithmParameters parameters) {

View File

@ -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);

View File

@ -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
*

View File

@ -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<String> 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 + "'");

View File

@ -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

View File

@ -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

View File

@ -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();

View File

@ -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) {

View File

@ -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<KeyType> 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,

View File

@ -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);

View File

@ -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<byte[]> 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<SNIServerName> getRequestedServerNames(QuicTLSEngineImpl engine) {
if (engine != null) {
return getRequestedServerNames(engine.getHandshakeSession());
}
return Collections.emptyList();
}
private static List<SNIServerName> getRequestedServerNames(
SSLSession session) {
if (session instanceof ExtendedSSLSession) {

View File

@ -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
#

View File

@ -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.
*
* <p id="ProtocolVersionSelection">
* 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.
*
* <p> If {@linkplain Version#HTTP_2 HTTP/2} is selected over a clear
* connection, and no HTTP/2 connection to the
* <a href="https://www.rfc-editor.org/rfc/rfc6454.html#section-4">origin server</a>
* 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.
*
* <p> 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.
* <p>
* 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.
* <br>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.
*
* <p id="UnsupportedProtocolVersion">
* 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.
*
* <p> If this method is not invoked prior to {@linkplain #build()
* building}, then newly built clients will prefer {@linkplain
* Version#HTTP_2 HTTP/2}.
*
* <p> 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
* <a href="https://tools.ietf.org/html/rfc6454#section-4">origin server</a>
* will use HTTP/2. If the upgrade fails, then the response will be
* handled using HTTP/1.1
* <p>If a request doesn't have a preferred version, then
* the effective preferred version for the request is the
* client's preferred version.</p>
*
* @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
}
/**

View File

@ -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.
*
* <p> 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.
* <p>
* 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 <T> The {@linkplain #type() type of the option value}
*
* @since 26
*/
public sealed interface HttpOption<T> 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<T> 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.
* <p>
* 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.
* <br>
* 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}.
* <p>
* In case of {@linkplain HttpClient.Redirect redirect}, the
* {@link #H3_DISCOVERY} option, if present, is always transferred to
* the new request.
* <p>
* 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<Http3DiscoveryMode> 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}.
* <p>
* 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.
* <p>
* When attempting an HTTP/3 connection in this mode, the {@code HttpClient} may
* use any <a href="https://www.rfc-editor.org/rfc/rfc7838">HTTP Alternative Services</a>
* 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
* <a href="https://www.rfc-editor.org/rfc/rfc7838">HTTP Alternative Services</a>
* 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.
* <p>
* In this mode, requests sent to the origin server will be sent through HTTP/1.1 or HTTP/2
* until a {@code h3} <a href="https://www.rfc-editor.org/rfc/rfc7838">HTTP Alternative Services</a>
* 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,
* <a href="https://www.rfc-editor.org/rfc/rfc7838">HTTP Alternative Services</a>
* are not used.
*/
HTTP_3_URI_ONLY
}
}

View File

@ -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 <T> 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 <T> Optional<T> getOption(HttpOption<T> option) {
Objects.requireNonNull(option);
return Optional.empty();
}
/**
* A builder of {@linkplain HttpRequest HTTP requests}.
*
@ -144,14 +163,53 @@ public abstract class HttpRequest {
*
* <p> 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.
*
* <p> 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.
* <p>
* 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 <T> Builder setOption(HttpOption<T> 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;
}

View File

@ -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<T>(Class<T> type, String name)
implements HttpOption<T> {
@Override
public String toString() {
return name();
}
}

View File

@ -803,24 +803,66 @@ public interface HttpResponse<T> {
/**
* A handler for push promises.
*
* <p> A <i>push promise</i> is a synthetic request sent by an HTTP/2 server
* <p> A <i>push promise</i> 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.
*
* <p> A push promise request may be received up to the point where the
* <p>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.
* <p>
* 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 <T> the push promise response body type
* @since 11
*/
public interface PushPromiseHandler<T> {
/**
* 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
* <a href="https://www.rfc-editor.org/rfc/rfc9114#server-push">RFC 9114,
* section 4.6</a>
*
* @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<T> {
* then the push promise is rejected. The {@code acceptor} function will
* throw an {@code IllegalStateException} if invoked more than once.
*
* <p> 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<T> {
Function<HttpResponse.BodyHandler<T>,CompletableFuture<HttpResponse<T>>> 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)}.
*
* <p> 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.
*
* <p> 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.
*
* <p> 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<HttpResponse.BodyHandler<T>,CompletableFuture<HttpResponse<T>>> 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<T> {
*
* @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<T> {
* {@snippet :
* // Streams the response body to a File
* HttpResponse<Path> 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[]

View File

@ -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.
* <p>
* 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.
* <p>
* 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");
}
}
}

View File

@ -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);
}
}

View File

@ -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 @@
/**
* <h2>HTTP Client and WebSocket APIs</h2>
*
* <p> Provides high-level client interfaces to HTTP (versions 1.1 and 2) and
* <p> 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:
*
* <ul>
@ -37,10 +37,12 @@
* </ul>
*
* <p> The protocol-specific requirements are defined in the
* <a href="https://tools.ietf.org/html/rfc7540">Hypertext Transfer Protocol
* Version 2 (HTTP/2)</a>, the <a href="https://tools.ietf.org/html/rfc2616">
* <a href="https://www.rfc-editor.org/info/rfc9114">Hypertext Transfer Protocol
* Version 3 (HTTP/3)</a>, the <a href="https://www.rfc-editor.org/info/rfc7540">
* Hypertext Transfer Protocol Version 2 (HTTP/2)</a>, the
* <a href="https://www.rfc-editor.org/info/rfc2616">
* Hypertext Transfer Protocol (HTTP/1.1)</a>, and
* <a href="https://tools.ietf.org/html/rfc6455">The WebSocket Protocol</a>.
* <a href="https://www.rfc-editor.org/info/rfc6455">The WebSocket Protocol</a>.
*
* <p> In general, asynchronous tasks execute in either the thread invoking
* the operation, e.g. {@linkplain HttpClient#send(HttpRequest, BodyHandler)
@ -66,6 +68,15 @@
* <p> 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;

View File

@ -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<Origin, List<AltService>> 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<AltService> 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<AltService> 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<SNIServerName> 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<InvalidAltSvc, Void> {
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<InvalidAltSvc, Void> 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<AltService> services) {
Objects.requireNonNull(origin);
Objects.requireNonNull(services);
List<AltService> 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<AltService> svcs = services.stream()
.filter(AltService.class::isInstance) // filter null
.filter((s) -> keepAltServiceFor(origin, s));
List<AltService> 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<AltService> 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<AltService> 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<AltService> old = list == null ? Stream.empty() : list.stream();
List<AltService> 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<AltService> 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<AltService> 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<AltService> lookup(final Origin origin, final String alpn) {
return lookup(origin, Predicate.isEqual(alpn));
}
public Stream<AltService> 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<AltService> lookup(final Origin origin,
final Predicate<? super String> alpnMatcher) {
if (debug.on()) debug.log("looking up alt-service for: %s", origin);
final List<AltService> 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<AltService> 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<AltService> 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();
}
}
}

View File

@ -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<AltService> 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<String, String> 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<SNIServerName> 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<SNIServerName> 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<AltService> 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<SNIServerName> getSNIServerNames(final HttpConnection conn) {
final List<SNIServerName> 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<AltService> processHeaderValue(final Origin origin,
final String headerValue) {
final List<AltService> 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<String, String> 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<String, String> 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<String, String> 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);
}
}

View File

@ -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<T> {
volatile ExchangeImpl<T> exchImpl;
volatile CompletableFuture<? extends ExchangeImpl<T>> exchangeCF;
volatile CompletableFuture<Void> bodyIgnored;
volatile boolean streamLimitReached;
// used to record possible cancellation raised before the exchImpl
// has been established.
@ -74,11 +76,18 @@ final class Exchange<T> {
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<T> multi) {
this.request = request;
this.upgrading = false;
@ -110,9 +119,13 @@ final class Exchange<T> {
}
// 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<T> {
// 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<T> {
void closeConnection(Throwable error) {
HttpConnection connection;
HttpQuicConnection quicConnection;
Throwable cause;
synchronized (this) {
cause = this.cause;
@ -141,39 +156,64 @@ final class Exchange<T> {
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<T> {
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<T> {
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<T> {
// 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<T> {
private CompletableFuture<? extends ExchangeImpl<T>>
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<T> {
}
CompletableFuture<? extends ExchangeImpl<T>> 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<T> {
}
// 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<Response> responseAsync() {
return responseAsyncImpl(null);
@ -715,4 +768,13 @@ final class Exchange<T> {
String dbgString() {
return dbgTag;
}
final boolean isUnprocessedByPeer() {
return this.unprocessedByPeer;
}
// Marks the exchange as unprocessed by the peer
final void markUnprocessedByPeer() {
this.unprocessedByPeer = true;
}
}

View File

@ -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<T> {
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<T> e) {
// e == null means a http/2 pushed stream
this.exchange = e;
@ -98,23 +103,414 @@ abstract class ExchangeImpl<T> {
static <U> CompletableFuture<? extends ExchangeImpl<U>>
get(Exchange<U> 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<Http2Connection> 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<CompletableFuture<? extends ExchangeImpl<U>>> 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 <U> CompletableFuture<? extends ExchangeImpl<U>>
attemptHttp2Exchange(Exchange<U> exchange, HttpConnection connection) {
HttpRequestImpl request = exchange.request();
Http2ClientImpl c2 = exchange.client().client2(); // #### improve
CompletableFuture<Http2Connection> 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<CompletableFuture<? extends ExchangeImpl<U>>> fxi =
c2f.handle((h2c, t) -> createExchangeImpl(h2c, t, exchange, connection));
return fxi.thenCompose(x -> x);
}
private static <U> CompletableFuture<? extends ExchangeImpl<U>>
attemptHttp3Exchange(Exchange<U> 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<Http3Connection> c3f;
Supplier<CompletableFuture<Http2Connection>> 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<Http2Connection> returned by the Http2Client,
// we are using a Supplier<CompletableFuture<Http2Connection>> 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<CompletableFuture<? extends ExchangeImpl<U>>> 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<CompletableFuture<? extends ExchangeImpl<U>>> 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 <U> CompletableFuture<? extends ExchangeImpl<U>> firstToComplete(
Exchange<U> exchange,
HttpConnection connection,
Supplier<CompletableFuture<Http2Connection>> c2fs,
CompletableFuture<Http3Connection> 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<Http2Connection> 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<CompletableFuture<? extends ExchangeImpl<U>>> cfxi = cf.handle((r, t) -> {
if (debug.on()) {
debug.log("Checking which from HTTP/2 or HTTP/3 succeeded first");
}
CompletableFuture<? extends ExchangeImpl<U>> 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<CompletableFuture<? extends ExchangeImpl<U>>> 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<CompletableFuture<? extends ExchangeImpl<U>>> 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<CompletableFuture<? extends ExchangeImpl<U>>> 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<CompletableFuture<? extends ExchangeImpl<U>>> 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 <U> CompletableFuture<? extends ExchangeImpl<U>>
fallbackToHttp1OnTimeout(Http3Connection c,
Throwable t,
Exchange<U> 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 <U> CompletableFuture<? extends ExchangeImpl<U>>
createExchangeImpl(Http3Connection c,
Throwable t,
Exchange<U> 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 <U, T extends ExchangeImpl<U>> 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 <U> CompletableFuture<? extends ExchangeImpl<U>>
createExchangeImpl(Http2Connection c,
Throwable t,
@ -280,12 +676,4 @@ abstract class ExchangeImpl<T> {
// 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;
}
}

View File

@ -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;
}
}
}

View File

@ -244,7 +244,7 @@ class Http1Exchange<T> extends ExchangeImpl<T> {
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);

View File

@ -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
}

View File

@ -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);
}
}

View File

@ -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<QuicVersion> availableQuicVersions;
static {
// we default to QUIC v1 followed by QUIC v2, if no specific preference cannot be
// determined
final List<QuicVersion> 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<QuicVersion> 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<Http3Connection> pendingClose = ConcurrentHashMap.newKeySet();
private final Set<String> noH3 = ConcurrentHashMap.newKeySet();
private final QuicClient quicClient;
private volatile boolean closed;
private final AtomicReference<Throwable> 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<Http3Connection> 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<Waiter> waiters)
implements ConnectionRecovery {
PendingConnection(AltService altSvc, Exchange<?> exchange, ConcurrentLinkedQueue<Waiter> 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<Http3Connection> 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<Http3Connection> wrapForDebug(CompletableFuture<Http3Connection> 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<AltService> lookupAltSvc(HttpRequestImpl request) {
return client.registry()
.lookup(request.uri(), H3::equals)
.findFirst();
}
CompletableFuture<Http3Connection> 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<Http3Connection> 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<Http3Connection> 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());
};
}
}

View File

@ -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.
* <p>
* 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.
* <p>
* Properties that are exposed are JDK specifics and typically documented
* in the {@link java.net.http} module API documentation.
* <ol>
* <li><Properties specific to HTTP/3 typically start with {@code jdk.httpclient.http3.}</li>
* <li><Properties specific to Qpack typically start with {@code jdk.httpclient.qpack.}</li>
* </ol>
*
* @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);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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<String,Http3Connection> advertised = new ConcurrentHashMap<>();
/* Map key is "scheme:host:port" */
private final Map<String,Http3Connection> unadvertised = new ConcurrentHashMap<>();
private final Logger debug;
Http3ConnectionPool(Logger logger) {
this.debug = Objects.requireNonNull(logger);
}
// https:<host>:<port>
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<Http3Connection> values() {
return java.util.stream.Stream.concat(
advertised.values().stream(),
unadvertised.values().stream());
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,224 @@
/*
* 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.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import jdk.internal.net.http.AltServicesRegistry.AltService;
import jdk.internal.net.http.Http3ClientImpl.ConnectionRecovery;
import jdk.internal.net.http.Http3ClientImpl.PendingConnection;
import jdk.internal.net.http.Http3ClientImpl.StreamLimitReached;
import jdk.internal.net.http.common.Log;
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 keeps track of pending HTTP/3 connections
* to avoid making two connections to the same server
* in parallel. Methods in this class are not atomic.
* Therefore, it is expected that they will be called
* while holding a lock in order to ensure atomicity.
*/
class Http3PendingConnections {
private final Map<String,ConnectionRecovery> pendingAdvertised = new ConcurrentHashMap<>();
private final Map<String,ConnectionRecovery> pendingUnadvertised = new ConcurrentHashMap<>();
Http3PendingConnections() {}
// Called when recovery is needed for a given connection, with
// the request that got the StreamLimitException
// Should be called while holding Http3ClientImpl.lock
void streamLimitReached(String key, Http3Connection connection) {
var altSvc = connection.connection().getSourceAltService().orElse(null);
var advertised = altSvc != null && altSvc.wasAdvertised();
var queue = advertised ? pendingAdvertised : pendingUnadvertised;
queue.computeIfAbsent(key, k -> new StreamLimitReached(connection));
}
// Remove a ConnectionRecovery after the connection was established
// Should be called while holding Http3ClientImpl.lock
ConnectionRecovery removeCompleted(String connectionKey, Exchange<?> origExchange, Http3Connection conn) {
var altSvc = Optional.ofNullable(conn)
.map(Http3Connection::connection)
.flatMap(HttpQuicConnection::getSourceAltService)
.orElse(null);
var discovery = Optional.ofNullable(origExchange)
.map(Exchange::request)
.map(HttpRequestImpl::http3Discovery)
.orElse(null);
var advertised = (altSvc != null && altSvc.wasAdvertised())
|| discovery == ALT_SVC;
var sameOrigin = (altSvc != null && altSvc.originHasSameAuthority());
ConnectionRecovery recovered = null;
if (advertised) {
recovered = pendingAdvertised.remove(connectionKey);
}
if (discovery == ALT_SVC || recovered != null) return recovered;
if (altSvc == null) {
// for instance, there was an exception, so we don't
// know if there was an altSvc because conn == null
recovered = pendingAdvertised.get(connectionKey);
if (recovered instanceof PendingConnection pending) {
if (pending.exchange() == origExchange) {
pendingAdvertised.remove(connectionKey, recovered);
return recovered;
}
}
}
recovered = pendingUnadvertised.get(connectionKey);
if (recovered instanceof PendingConnection pending) {
if (pending.exchange() == origExchange) {
pendingUnadvertised.remove(connectionKey, recovered);
return pending;
}
}
if (!sameOrigin && advertised) return null;
return pendingUnadvertised.remove(connectionKey);
}
// Lookup a ConnectionRecovery for the given request with the
// given key.
// Should be called while holding Http3ClientImpl.lock
ConnectionRecovery lookupFor(String key, HttpRequestImpl request, HttpClientImpl client) {
var discovery = request.http3Discovery();
// if ALT_SVC only look in advertised
if (discovery == ALT_SVC) {
return pendingAdvertised.get(key);
}
// if HTTP_3_ONLY look first in pendingUnadvertised
var unadvertised = pendingUnadvertised.get(key);
if (discovery == HTTP_3_URI_ONLY && unadvertised != null) {
if (unadvertised instanceof PendingConnection) {
return unadvertised;
}
}
// then look in advertised
var advertised = pendingAdvertised.get(key);
if (advertised instanceof PendingConnection pending) {
var altSvc = pending.altSvc();
var sameOrigin = altSvc != null && altSvc.originHasSameAuthority();
assert altSvc != null; // pending advertised should have altSvc
if (discovery == ANY || sameOrigin) return advertised;
}
// if HTTP_3_ONLY, nothing found, stop here
assert discovery != HTTP_3_URI_ONLY || !(unadvertised instanceof PendingConnection);
if (discovery == HTTP_3_URI_ONLY) {
if (advertised != null && Log.http3()) {
Log.logHttp3("{0} cannot be used for {1}: return null", advertised, request);
}
assert !(unadvertised instanceof PendingConnection);
return unadvertised;
}
// if ANY return advertised if found, otherwise unadvertised
if (advertised instanceof PendingConnection) return advertised;
if (unadvertised instanceof PendingConnection) {
if (client.client3().isEmpty()) {
return unadvertised;
}
// if ANY and we have an alt service that's eligible for the request
// and is not same origin as the request's URI authority, then don't
// return unadvertised and instead return advertised (which may be null)
final AltService altSvc = client.client3().get().lookupAltSvc(request).orElse(null);
if (altSvc != null && !altSvc.originHasSameAuthority()) {
return advertised;
} else {
return unadvertised;
}
}
if (advertised != null) return advertised;
return unadvertised;
}
// Adds a pending connection for the given request with the given
// key and altSvc.
// Should be called while holding Http3ClientImpl.lock
PendingConnection addPending(String key, HttpRequestImpl request, AltService altSvc, Exchange<?> exchange) {
var discovery = request.http3Discovery();
var advertised = altSvc != null && altSvc.wasAdvertised();
var sameOrigin = altSvc == null || altSvc.originHasSameAuthority();
// if advertised and same origin, we don't use pendingUnadvertised
// but pendingAdvertised even if discovery is HTTP_3_URI_ONLY
// if we have an advertised altSvc with not same origin, we still
// want to attempt HTTP_3_URI_ONLY at origin, as an unadvertised
// connection. If advertised & same origin, we can use the advertised
// service instead and use pendingAdvertised, even for HTTP_3_URI_ONLY
if (discovery == HTTP_3_URI_ONLY && (!advertised || !sameOrigin)) {
PendingConnection pendingConnection = new PendingConnection(null, exchange);
var previous = pendingUnadvertised.put(key, pendingConnection);
if (previous instanceof PendingConnection prev) {
String msg = "previous unadvertised pending connection found!"
+ " (originally created for %s #%s) while adding pending connection for %s"
.formatted(prev.exchange().request, prev.exchange().multi.id, exchange.multi.id);
if (Log.errors()) Log.logError(msg);
assert false : msg;
}
return pendingConnection;
}
assert discovery != HTTP_3_URI_ONLY || advertised && sameOrigin;
if (advertised) {
PendingConnection pendingConnection = new PendingConnection(altSvc, exchange);
var previous = pendingAdvertised.put(key, pendingConnection);
if (previous instanceof PendingConnection prev) {
String msg = "previous pending advertised connection found!"
+ " (originally created for %s #%s) while adding pending connection for %s"
.formatted(prev.exchange().request, prev.exchange().multi.id, exchange.multi.id);
if (Log.errors()) Log.logError(msg);
assert false : msg;
}
return pendingConnection;
}
if (discovery == ANY) {
assert !advertised;
PendingConnection pendingConnection = new PendingConnection(null, exchange);
var previous = pendingUnadvertised.put(key, pendingConnection);
if (previous instanceof PendingConnection prev) {
String msg = ("previous unadvertised pending connection found for ANY!" +
" (originally created for %s #%s) while adding pending connection for %s")
.formatted(prev.exchange().request, prev.exchange().multi.id, exchange.multi.id);
if (Log.errors()) Log.logError(msg);
assert false : msg;
}
return pendingConnection;
}
// last case - if we reach here we're ALT_SVC but couldn't
// find an advertised alt service.
assert discovery == ALT_SVC;
return null;
}
}

View File

@ -0,0 +1,811 @@
/*
* 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 java.io.IOException;
import java.net.ProtocolException;
import java.net.http.HttpHeaders;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandler;
import java.net.http.HttpResponse.PushPromiseHandler.PushId;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantLock;
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.streams.QuicReceiverStream;
import static jdk.internal.net.http.Http3ClientProperties.MAX_HTTP3_PUSH_STREAMS;
/**
* Manages HTTP/3 push promises for an HTTP/3 connection.
* <p>
* This class maintains a bounded collection of recent push promises,
* together with the current state of the promise: pending, processed, or
* cancelled. When a new {@link jdk.internal.net.http.http3.frames.PushPromiseFrame}
* is received, and entry is added in the map, and the state of the promise
* is updated as it goes.
* When the map is full, old entries (lowest pushId) are expunged from
* the map. No promise will be accepted if its pushId is lower than the
* lowest pushId in the map.
*
* @apiNote
* When a PushPromiseFrame is received, {@link
* #onPushPromiseFrame(Http3ExchangeImpl, long, HttpHeaders)}
* is called. This arranges for an entry to be added to the map, unless there's
* already one. Also, the first Http3ExchangeImpl for which this method is called
* for a given pushId gets to handle the PushPromise: its {@link
* java.net.http.HttpResponse.PushPromiseHandler} will be invoked to accept the promise
* and handle the body.
* <p>
* When a new PushStream is opened, {@link #onPushPromiseStream(QuicReceiverStream, long)}
* is called. When both {@code onPushPromiseFrame} and {@code onPushPromiseStream} have
* been called for a given {@code pushId}, an {@link Http3PushPromiseStream} is created
* and started to receive the body.
* <p>
* {@link Http3ExchangeImpl} that receive a push promise frame, but don't get to handle
* the body (because it's already been delegated to another stream) should call
* {@link #whenAccepted(long)} to figure out when it is safe to invoke {@link
* PushGroup#acceptPushPromiseId(PushId)}.
* <p>
* {@link #cancelPushPromise(long, Throwable, CancelPushReason)} can be called to cancel
* a push promise. {@link #pushPromiseProcessed(long)} should be called when the body
* has been fully processed.
*/
final class Http3PushManager {
private final Logger debug = Utils.getDebugLogger(this::dbgTag);
private final ReentrantLock promiseLock = new ReentrantLock();
private final ConcurrentHashMap<Long, PushPromise> promises = new ConcurrentHashMap<>();
private final CompletableFuture<Boolean> DENIED = MinimalFuture.completedFuture(Boolean.FALSE);
private final CompletableFuture<Boolean> ACCEPTED = MinimalFuture.completedFuture(Boolean.TRUE);
private final AtomicLong maxPushId = new AtomicLong();
private final AtomicLong maxPushReceived = new AtomicLong();
private final AtomicLong minPushId = new AtomicLong();
// the max history we keep in the promiseMap. We start expunging old
// entries from the map when the size of the map exceeds this value
private static final long MAX_PUSH_HISTORY_SIZE = (3*MAX_HTTP3_PUSH_STREAMS)/2;
// the maxPushId increments, we send on MAX_PUSH_ID frame
// with a maxPushId incremented by that amount.
// Ideally should be <= to MAX_PUSH_HISTORY_SIZE, to avoid
// filling up the history right after the first MAX_PUSH_ID
private static final long MAX_PUSH_ID_INCREMENTS = MAX_HTTP3_PUSH_STREAMS;
private final Http3Connection connection;
// number of pending promises
private final AtomicInteger pendingPromises = new AtomicInteger();
// push promises are considered blocked if we have failed to send
// the last MAX_PUSH_ID update due to pendingPromises
// count having reached MAX_HTTP3_PUSH_STREAMS
private volatile boolean pushPromisesBlocked;
Http3PushManager(Http3Connection connection) {
this.connection = connection;
}
String dbgTag() {
return connection.dbgTag();
}
public void cancelAllPromises(IOException closeCause, Http3Error error) {
for (var promise : promises.entrySet()) {
var pushId = promise.getKey();
var pp = promise.getValue();
switch (pp) {
case ProcessedPushPromise ignored -> {}
case CancelledPushPromise ignored -> {}
case PendingPushPromise<?> ppp -> {
cancelPendingPushPromise(ppp, closeCause);
}
}
}
}
// Different actions needs to be carried out when cancelling a
// push promise, depending on the state of the promise and the
// cancellation reason.
enum CancelPushReason {
NO_HANDLER, // the exchange has no PushGroup
PUSH_CANCELLED, // the PromiseHandler cancelled the push,
// or an error occurred handling the promise
CANCEL_RECEIVED; // received CANCEL_PUSH from server
}
/**
* A PushPromise can be a PendingPushPromise, until the push
* response is completely received, or a ProcessedPushPromise,
* which replace the PendingPushPromise after the response body
* has been delivered. If the PushPromise is cancelled before
* accepting it or receiving a body, CancelledPushPromise will
* be recorded and replace the PendingPushPromise.
*/
private sealed interface PushPromise
permits PendingPushPromise, ProcessedPushPromise, CancelledPushPromise {
}
/**
* Represent a PushPromise whose body as already been delivered
*/
private record ProcessedPushPromise(PushId pushId, HttpHeaders promiseHeaders)
implements PushPromise { }
/**
* Represent a PushPromise that has been cancelled. No body will be delivered.
*/
private record CancelledPushPromise(PushId pushId) implements PushPromise { }
// difficult to say what will come first - the push promise,
// or the push stream?
// The first push promise frame received will register the
// exchange with this class - and trigger the parsing of
// the request/response when the stream is available.
// The other will trigger a simple call to register the
// push id.
// Probably we also need some timer to clean
// up the map if the stream doesn't manifest after a while.
// We maintain minPushID, where any frame
// containing a push id < to the min will be discarded,
// and any stream with a pushId < will also be discarded.
/**
* Represents a PushPromise whose body has not been delivered
* yet.
* @param <T> the type of the body
*/
private static final class PendingPushPromise<T> implements PushPromise {
// called when the first push promise frame is received
PendingPushPromise(Http3ExchangeImpl<T> exchange, long pushId, HttpHeaders promiseHeaders) {
this.accepted = new MinimalFuture<>();
this.exchange = Objects.requireNonNull(exchange);
this.promiseHeaders = Objects.requireNonNull(promiseHeaders);
this.pushId = pushId;
}
// called when the push promise stream is opened
PendingPushPromise(QuicReceiverStream stream, long pushId) {
this.accepted = new MinimalFuture<>();
this.stream = Objects.requireNonNull(stream);
this.pushId = pushId;
}
// volatiles should not be required since we only modify/read
// those within a lock. Final fields should ensure safe publication
final long pushId; // the push id
QuicReceiverStream stream; // the quic promise stream
Http3ExchangeImpl<T> exchange; // the exchange that will create the body subscriber
Http3PushPromiseStream<T> promiseStream; // the HTTP/3 stream to process the quic stream
HttpHeaders promiseHeaders; // the push promise request headers
CompletableFuture<HttpResponse<T>> responseCF;
HttpRequestImpl pushReq;
BodyHandler<T> handler;
final CompletableFuture<Boolean> accepted; // whether the push promise was accepted
public long pushId() { return pushId; }
public boolean ready() {
if (stream == null) return false;
if (exchange == null) return false;
if (promiseHeaders == null) return false;
if (!accepted.isDone()) return false;
if (responseCF == null) return false;
if (pushReq == null) return false;
if (handler == null) return false;
return true;
}
@Override
public String toString() {
return "PendingPushPromise{" +
"pushId=" + pushId +
", stream=" + stream +
", exchange=" + dbgTag(exchange) +
", promiseStream=" + dbgTag(promiseStream) +
", promiseHeaders=" + promiseHeaders +
", accepted=" + accepted +
'}';
}
String dbgTag(Http3ExchangeImpl<?> exchange) {
return exchange == null ? null : exchange.dbgTag();
}
String dbgTag(Http3PushPromiseStream<?> promiseStream) {
return promiseStream == null ? null : promiseStream.dbgTag();
}
}
/**
* {@return the maximum pushId that can be accepted from the peer}
* This corresponds to the pushId that has been included in the last
* MAX_PUSH_ID frame sent to the peer. A pushId greater than this
* value must be rejected, and cause the connection to close with
* error.
*
* @apiNote due to internal constraints it is possible that the
* MAX_PUSH_ID frame has not been sent yet, but the {@code Http3PushManager}
* will behave as if the peer had received that frame.
*
* @see Http3Connection#checkMaxPushId(long)
* @see #checkMaxPushId(long)
*/
long getMaxPushId() {
return maxPushId.get();
}
/**
* {@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.
*/
long getMinPushId() {
return minPushId.get();
}
/**
* 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;
if (!connection.acceptLargerPushPromise(pushStream, pushId)) return;
PendingPushPromise<?> promise = addPushPromise(pushStream, pushId);
if (promise != null) {
assert promise.stream == pushStream;
// if stream is avoilable start parsing?
tryReceivePromise(promise);
}
}
/**
* Checks whether a MAX_PUSH_ID frame needs to be sent,
* and send it.
* Called from {@link Http3Connection#checkSendMaxPushId()}.
*/
void checkSendMaxPushId() {
if (MAX_PUSH_ID_INCREMENTS <= 0) return;
long pendingCount = pendingPromises.get();
long availableSlots = MAX_HTTP3_PUSH_STREAMS - pendingCount;
if (availableSlots <= 0) {
pushPromisesBlocked = true;
if (debug.on()) debug.log("Push promises blocked: availableSlots=%s", pendingCount);
return;
}
long maxPushIdSent = maxPushId.get();
long maxPushIdReceived = maxPushReceived.get();
long half = Math.max(1, MAX_PUSH_ID_INCREMENTS /2);
if (maxPushIdSent - maxPushIdReceived < half) {
// do not send a maxPushId that would consume more
// than our available slots
long increment = Math.min(availableSlots, MAX_PUSH_ID_INCREMENTS);
long update = maxPushIdSent + increment;
boolean updated = false;
try {
// let's update the counter before sending the frame,
// otherwise there's a chance we can receive a frame
// before updating the counter.
do {
if (maxPushId.compareAndSet(maxPushIdSent, update)) {
if (debug.on()) {
debug.log("MAX_PUSH_ID updated: %s (%s -> %s), increment %s, pending %s, availableSlots %s",
update, maxPushIdSent, update, increment,
promises.values().stream().filter(PendingPushPromise.class::isInstance)
.map(p -> (PendingPushPromise<?>) p)
.map(PendingPushPromise::pushId).toList(),
availableSlots);
}
updated = true;
break;
}
maxPushIdSent = maxPushId.get();
} while (maxPushIdSent < update);
if (updated) {
if (pushPromisesBlocked) {
if (debug.on()) debug.log("Push promises unblocked: maxPushIdSent=%s", update);
pushPromisesBlocked = false;
}
connection.sendMaxPushId(update);
}
} catch (IOException io) {
debug.log("Failed to send MAX_PUSH_ID(%s): %s", update, io);
}
}
}
/**
* Called when a PushPromiseFrame has been decoded.
*
* @apiNote
* This method calls {@link Http3ExchangeImpl#acceptPushPromise(long, HttpRequestImpl)}
* and {@link Http3ExchangeImpl#onPushRequestAccepted(long, CompletableFuture)}
* for the first exchange that receives the {@link
* jdk.internal.net.http.http3.frames.PushPromiseFrame}
*
* @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
*
* @see Http3Connection#onPushPromiseFrame(Http3ExchangeImpl, long, HttpHeaders)
*/
<U> boolean onPushPromiseFrame(Http3ExchangeImpl<U> exchange, long pushId, HttpHeaders promiseHeaders)
throws IOException {
if (!connection.acceptLargerPushPromise(null, pushId)) return false;
PendingPushPromise<?> promise = addPushPromise(exchange, pushId, promiseHeaders);
if (promise == null) {
return false;
}
// A PendingPushPromise is returned only if there was no
// PushPromise present. If a PendingPushPromise is returned
// it should therefore have its exchange already set to the
// current exchange.
assert promise.exchange == exchange;
HttpRequestImpl pushReq = HttpRequestImpl.createPushRequest(
exchange.getExchange().request(), promiseHeaders);
var acceptor = exchange.acceptPushPromise(pushId, pushReq);
if (acceptor == null) {
// nothing to do: the push should already have been cancelled.
return false;
}
@SuppressWarnings("unchecked")
var pppU = (PendingPushPromise<U>) promise;
var responseCF = pppU.responseCF;
assert responseCF == null;
boolean cancelled = false;
promiseLock.lock();
try {
promise.pushReq = pushReq;
pppU.responseCF = responseCF = acceptor.cf();
// recheck to verify the push hasn't been cancelled already
var check = promises.get(pushId);
if (check instanceof CancelledPushPromise || check == null) {
cancelled = true;
} else {
assert promise == check;
pppU.handler = acceptor.bodyHandler();
}
} finally {
promiseLock.unlock();
}
if (!cancelled) {
exchange.onPushRequestAccepted(pushId, responseCF);
promise.accepted.complete(true);
// if stream is available start parsing?
tryReceivePromise(promise);
return true;
} else {
cancelPendingPushPromise(promise, null);
// should be a no-op - in theory it should already
// have been completed
promise.accepted.complete(false);
return false;
}
}
/**
* {@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.
*
* 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}.
*
* @see Http3Connection#whenPushAccepted(long)
* @param pushId the pushId
*/
CompletableFuture<Boolean> whenAccepted(long pushId) {
var promise = promises.get(pushId);
if (promise instanceof PendingPushPromise<?> pp) {
return pp.accepted;
} else if (promise instanceof ProcessedPushPromise) {
return ACCEPTED;
} else { // CancelledPushPromise or null
return DENIED;
}
}
/**
* Cancel a push promise. In case of concurrent requests receiving the
* same pushId, where one has a PushPromiseHandler and the other doesn't,
* we will cancel the push only if reason != CANCEL_RECEIVED, or no request
* stream has already accepted the push.
*
* @param pushId the promise pushId
* @param cause the cause (can be null)
* @param reason reason for cancelling
*/
void cancelPushPromise(long pushId, Throwable cause, CancelPushReason reason) {
boolean sendCancelPush = false;
PendingPushPromise<?> pending = null;
if (cause != null) {
debug.log("PushPromise cancelled: pushId=" + pushId, cause);
} else {
debug.log("PushPromise cancelled: pushId=%s", pushId);
String msg = "cancelPushPromise(pushId="+pushId+")";
debug.log(msg);
}
if (reason == CancelPushReason.CANCEL_RECEIVED) {
if (checkMaxPushId(pushId) != null) {
// pushId >= max connection will be closed
return;
}
}
promiseLock.lock();
try {
var promise = promises.get(pushId);
long min = minPushId.get();
if (promise == null) {
if (pushId > maxPushReceived.get()) maxPushReceived.set(pushId);
checkExpungePromiseMap();
if (pushId >= min) {
var cancelled = new CancelledPushPromise(connection.newPushId(pushId));
promises.put(pushId, cancelled);
sendCancelPush = reason != CancelPushReason.CANCEL_RECEIVED;
}
} else if (promise instanceof CancelledPushPromise) {
// nothing to do
} else if (promise instanceof ProcessedPushPromise) {
// nothing we can do?
} else if (promise instanceof PendingPushPromise<?> ppp) {
// only cancel if never accepted, or force cancel requested
if (ppp.promiseStream == null || reason != CancelPushReason.NO_HANDLER) {
var cancelled = new CancelledPushPromise(connection.newPushId(pushId));
promises.put(pushId, cancelled);
long pendingCount = pendingPromises.decrementAndGet();
long ppc;
assert (ppc = promises.values().stream().filter(PendingPushPromise.class::isInstance).count()) == pendingCount
: "bad pending promise count: expected %s but found %s".formatted(pendingCount, ppc);
ppp.accepted.complete(false); // NO OP if already completed
pending = ppp;
// send cancel push; do not send if we received
// a CancelPushFrame from the peer
// also do not update MAX_PUSH_ID here - MAX_PUSH_ID will
// be updated when starting the next request/response exchange that accepts
// push promises.
sendCancelPush = reason != CancelPushReason.CANCEL_RECEIVED;
}
}
} finally {
promiseLock.unlock();
}
if (sendCancelPush) {
connection.sendCancelPush(pushId, cause);
}
if (pending != null) {
cancelPendingPushPromise(pending, cause);
}
}
private void cancelPendingPushPromise(PendingPushPromise<?> ppp, Throwable cause) {
var ps = ppp.stream;
var http3 = ppp.promiseStream;
var responseCF = ppp.responseCF;
if (ps != null) {
ps.requestStopSending(Http3Error.H3_REQUEST_CANCELLED.code());
}
if (http3 != null || responseCF != null) {
IOException io;
if (cause == null) {
io = new IOException("Push promise cancelled: " + ppp.pushId);
} else {
io = Utils.toIOException(cause);
}
if (http3 != null) {
http3.cancel(io);
} else if (responseCF != null) {
responseCF.completeExceptionally(io);
}
}
}
/**
* Called when a push promise response body has been successfully received.
* @param pushId the pushId
*/
void pushPromiseProcessed(long pushId) {
promiseLock.lock();
try {
var promise = promises.get(pushId);
if (promise instanceof PendingPushPromise<?> ppp) {
var processed = new ProcessedPushPromise(connection.newPushId(pushId),
ppp.promiseHeaders);
promises.put(pushId, processed);
var pendingCount = pendingPromises.decrementAndGet();
long ppc;
assert (ppc = promises.values().stream().filter(PendingPushPromise.class::isInstance).count()) == pendingCount
: "bad pending promise count: expected %s but found %s".formatted(pendingCount, ppc);
// do not update MAX_PUSH_ID here - MAX_PUSH_ID will
// be updated when starting the next request/response exchange that accepts
// push promises.
}
} finally {
promiseLock.unlock();
}
}
/**
* 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 connection.checkMaxPushId(pushId);
}
// Checks whether an Http3PushPromiseStream can be created now
private <U> void tryReceivePromise(PendingPushPromise<U> promise) {
debug.log("tryReceivePromise: " + promise);
promiseLock.lock();
Http3PushPromiseStream<U> http3PushPromiseStream = null;
IOException failed = null;
try {
if (promise.ready() && promise.promiseStream == null) {
promise.promiseStream = http3PushPromiseStream =
createPushExchange(promise);
} else {
debug.log("tryReceivePromise: Can't create Http3PushPromiseStream for pushId=%s yet",
promise.pushId);
}
} catch (IOException io) {
failed = io;
} finally {
promiseLock.unlock();
}
if (failed != null) {
cancelPushPromise(promise.pushId, failed, CancelPushReason.PUSH_CANCELLED);
return;
}
if (http3PushPromiseStream != null) {
// HTTP/3 push promises are not ref-counted
// If we were to change that it could be necessary to
// temporarly increment ref-counting here, until the stream
// read loop effectively starts.
http3PushPromiseStream.start();
}
}
// try to create and start an Http3PushPromiseStream when all bits have
// been received
private <U> Http3PushPromiseStream<U> createPushExchange(PendingPushPromise<U> promise)
throws IOException {
assert promise.ready() : "promise is not ready: " + promise;
Http3ExchangeImpl<U> parent = promise.exchange;
HttpRequestImpl pushReq = promise.pushReq;
QuicReceiverStream quicStream = promise.stream;
Exchange<U> pushExch = new Exchange<>(pushReq, parent.exchange.multi);
Http3PushPromiseStream<U> pushStream = new Http3PushPromiseStream<>(pushExch,
parent.http3Connection(), this,
quicStream, promise.responseCF, promise.handler, parent, promise.pushId);
pushExch.exchImpl = pushStream;
return pushStream;
}
// The first exchange that gets the PushPromise gets a PushPromise object,
// others get null
// TODO: ideally we should start a timer to cancel a push promise if
// the stream doesn't materialize after a while.
// Note that the callers can always start their own timeouts using
// the CompletableFutures we returned to them.
private <U> PendingPushPromise<U> addPushPromise(Http3ExchangeImpl<U> exchange,
long pushId,
HttpHeaders promiseHeaders) {
PushPromise promise = promises.get(pushId);
boolean cancelStream = false;
if (promise == null) {
promiseLock.lock();
try {
promise = promises.get(pushId);
if (promise == null) {
if (checkMaxPushId(pushId) == null) {
if (pushId >= minPushId.get()) {
if (pushId > maxPushReceived.get()) maxPushReceived.set(pushId);
checkExpungePromiseMap();
var pp = new PendingPushPromise<>(exchange, pushId, promiseHeaders);
promises.put(pushId, pp);
long pendingCount = pendingPromises.incrementAndGet();
long ppc;
assert (ppc = promises.values().stream().filter(PendingPushPromise.class::isInstance).count()) == pendingCount
: "bad pending promise count: expected %s but found %s".formatted(pendingCount, ppc);
return pp;
} else {
// pushId < minPushId
cancelStream = true;
}
} else return null;
}
} finally {
promiseLock.unlock();
}
}
if (cancelStream) {
// we don't have the stream;
// the stream will be canceled if it comes later
// do not send push cancel frame (already cancelled, or abandoned)
return null;
}
if (promise instanceof PendingPushPromise<?> ppp) {
var pe = ppp.exchange;
if (pe == null) {
promiseLock.lock();
try {
if (ppp.exchange == null) {
assert ppp.promiseHeaders == null;
@SuppressWarnings("unchecked")
var pppU = (PendingPushPromise<U>) ppp;
pppU.exchange = exchange;
pppU.promiseHeaders = promiseHeaders;
return pppU;
}
} finally {
promiseLock.unlock();
}
}
var previousHeaders = ppp.promiseHeaders;
if (previousHeaders != null && !previousHeaders.equals(promiseHeaders)) {
connection.protocolError(
new ProtocolException("push headers do not match with previous promise for " + pushId));
}
} else if (promise instanceof ProcessedPushPromise ppp) {
if (!ppp.promiseHeaders().equals(promiseHeaders)) {
connection.protocolError(
new ProtocolException("push headers do not match with previous promise for " + pushId));
}
} else if (promise instanceof CancelledPushPromise) {
// already cancelled - nothing to do
}
return null;
}
// TODO: the packet opening the push promise stream might reach us before
// the push promise headers are processed. We could start a timer
// here to cancel the push promise if the PushPromiseFrame doesn't materialize
// after a while.
private <U> PendingPushPromise<U> addPushPromise(QuicReceiverStream stream, long pushId) {
PushPromise promise = promises.get(pushId);
boolean cancelStream = false;
if (promise == null) {
promiseLock.lock();
try {
promise = promises.get(pushId);
if (promise == null) {
if (checkMaxPushId(pushId) == null) {
if (pushId >= minPushId.get()) {
if (pushId > maxPushReceived.get()) maxPushReceived.set(pushId);
checkExpungePromiseMap();
var pp = new PendingPushPromise<U>(stream, pushId);
promises.put(pushId, pp);
long pendingCount = pendingPromises.incrementAndGet();
long ppc;
assert (ppc = promises.values().stream().filter(PendingPushPromise.class::isInstance).count()) == pendingCount
: "bad pending promise count: expected %s but found %s".formatted(pendingCount, ppc);
return pp;
} else {
// pushId < minPushId
cancelStream = true;
}
} else return null; // maxPushId exceeded, connection closed
}
} finally {
promiseLock.unlock();
}
}
if (cancelStream) {
// do not send push cancel frame (already cancelled, or abandoned)
stream.requestStopSending(Http3Error.H3_REQUEST_CANCELLED.code());
return null;
}
if (promise instanceof PendingPushPromise<?> ppp) {
var ps = ppp.stream;
if (ps == null) {
promiseLock.lock();
try {
if ((ps = ppp.stream) == null) {
ps = ppp.stream = stream;
}
} finally {
promiseLock.unlock();
}
}
if (ps == stream) {
@SuppressWarnings("unchecked")
var pp = ((PendingPushPromise<U>) ppp);
return pp;
} else {
// Error! cancel stream...
var io = new ProtocolException("HTTP/3 pushId %s already used on this connection".formatted(pushId));
connection.connectionError(io, Http3Error.H3_ID_ERROR);
}
} else if (promise instanceof ProcessedPushPromise) {
var io = new ProtocolException("HTTP/3 pushId %s already used on this connection".formatted(pushId));
connection.connectionError(io, Http3Error.H3_ID_ERROR);
} else {
// already cancelled?
// Error! cancel stream...
// connection.sendCancelPush(pushId, null);
stream.requestStopSending(Http3Error.H3_REQUEST_CANCELLED.code());
}
return null;
}
// We only keep MAX_PUSH_HISTORY_SIZE entries in the map.
// If the map has more than MAX_PUSH_HISTORY_SIZE entries, we start expunging
// pushIds starting at minPushId. This method makes room for at least
// on push promise in the map
private void checkExpungePromiseMap() {
assert promiseLock.isHeldByCurrentThread();
while (promises.size() >= MAX_PUSH_HISTORY_SIZE) {
long min = minPushId.getAndIncrement();
var pp = promises.remove(min);
if (pp instanceof PendingPushPromise<?> ppp) {
var pendingCount = pendingPromises.decrementAndGet();
long ppc;
assert (ppc = promises.values().stream().filter(PendingPushPromise.class::isInstance).count()) == pendingCount
: "bad pending promise count: expected %s but found %s".formatted(pendingCount, ppc);
var http3 = ppp.promiseStream;
IOException io = null;
if (http3 != null) {
http3.cancel(io = new IOException("PushPromise cancelled"));
}
if (io == null) {
io = new IOException("PushPromise cancelled");
}
connection.sendCancelPush(ppp.pushId, io);
var ps = ppp.stream;
if (ps != null) {
ps.requestStopSending(Http3Error.H3_REQUEST_CANCELLED.code());
}
}
}
}
}

View File

@ -0,0 +1,746 @@
/*
* 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.net.ProtocolException;
import java.net.http.HttpClient.Version;
import java.net.http.HttpHeaders;
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.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantLock;
import jdk.internal.net.http.Http3PushManager.CancelPushReason;
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.http3.Http3Error;
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.readers.HeaderFrameReader;
import jdk.internal.net.http.quic.streams.QuicReceiverStream;
import jdk.internal.net.http.quic.streams.QuicStreamReader;
import static jdk.internal.net.http.http3.Http3Error.H3_FRAME_UNEXPECTED;
/**
* This class represents an HTTP/3 PushPromise stream.
*/
final class Http3PushPromiseStream<T> extends Http3Stream<T> {
private final Logger debug = Utils.getDebugLogger(this::dbgTag);
private final Http3Connection connection;
private final HttpHeadersBuilder respHeadersBuilder;
private final PushRespHeadersConsumer respHeadersConsumer;
private final HeaderFrameReader respHeaderFrameReader;
private final Decoder qpackDecoder;
private final AtomicReference<Throwable> errorRef;
private final CompletableFuture<Response> pushCF = new MinimalFuture<>();
private final CompletableFuture<HttpResponse<T>> responseCF;
private final QuicReceiverStream stream;
private final QuicStreamReader reader;
private final Http3ExchangeImpl<T> parent;
private final long pushId;
private final Http3PushManager pushManager;
private final BodyHandler<T> pushHandler;
private final FramesDecoder framesDecoder =
new FramesDecoder(this::dbgTag, FramesDecoder::isAllowedOnPromiseStream);
private final SequentialScheduler readScheduler =
SequentialScheduler.lockingScheduler(this::processQuicData);
private final ReentrantLock stateLock = new ReentrantLock();
private final H3FrameOrderVerifier frameOrderVerifier = H3FrameOrderVerifier.newForPushPromiseStream();
final SubscriptionBase userSubscription =
new SubscriptionBase(readScheduler, this::cancel, this::onSubscriptionError);
volatile boolean closed;
volatile BodySubscriber<T> pendingResponseSubscriber;
volatile BodySubscriber<T> responseSubscriber;
volatile CompletableFuture<T> responseBodyCF;
volatile boolean responseReceived;
volatile int responseCode;
volatile Response response;
volatile boolean stopRequested;
private String dbgTag = null;
Http3PushPromiseStream(Exchange<T> exchange,
final Http3Connection connection,
final Http3PushManager pushManager,
final QuicReceiverStream stream,
final CompletableFuture<HttpResponse<T>> responseCF,
final BodyHandler<T> pushHandler,
Http3ExchangeImpl<T> parent,
long pushId) {
super(exchange);
this.responseCF = responseCF;
this.pushHandler = pushHandler;
this.errorRef = new AtomicReference<>();
this.pushId = pushId;
this.connection = connection;
this.pushManager = pushManager;
this.stream = stream;
this.parent = parent;
this.respHeadersBuilder = new HttpHeadersBuilder();
this.respHeadersConsumer = new PushRespHeadersConsumer();
this.qpackDecoder = connection.qpackDecoder();
this.respHeaderFrameReader = qpackDecoder.newHeaderFrameReader(respHeadersConsumer);
this.reader = stream.connectReader(readScheduler);
debug.log("Http3PushPromiseStream created");
}
void start() {
exchange.exchImpl = this;
parent.onHttp3PushStreamStarted(exchange.request(), this);
this.reader.start();
}
long pushId() {
return pushId;
}
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 = "Http3PushPromiseStream(" + ctag + ", streamId=" + sid + ", pushId="+ pushId + ")";
if (streamId == -1) return tag;
return dbgTag = tag;
}
@Override
long streamId() {
var stream = this.stream;
return stream == null ? -1 : stream.streamId();
}
private final class PushRespHeadersConsumer extends StreamHeadersConsumer {
public PushRespHeadersConsumer() {
super(Context.RESPONSE);
}
void resetDone() {
if (debug.on()) {
debug.log("Response builder cleared, ready to receive new headers.");
}
}
@Override
String headerFieldType() {
return "PUSH RESPONSE HEADER FIELD";
}
@Override
Decoder qpackDecoder() {
return qpackDecoder;
}
@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 response: " + super.formatMessage(message, header);
}
@Override
HeaderFrameReader headerFrameReader() {
return respHeaderFrameReader;
}
@Override
HttpHeadersBuilder headersBuilder() {
return respHeadersBuilder;
}
@Override
void headersCompleted() {
handleResponse();
}
@Override
public long streamId() {
return stream.streamId();
}
}
@Override
HttpQuicConnection connection() {
return connection.connection();
}
// The Http3StreamResponseSubscriber is registered with the HttpClient
// to ensure that it gets completed if the SelectorManager aborts due
// to unexpected exceptions.
private void registerResponseSubscriber(Http3PushStreamResponseSubscriber<?> subscriber) {
if (client().registerSubscriber(subscriber)) {
debug.log("Reference response body for h3 stream: " + streamId());
client().h3StreamReference();
}
}
private void unregisterResponseSubscriber(Http3PushStreamResponseSubscriber<?> subscriber) {
if (client().unregisterSubscriber(subscriber)) {
debug.log("Unreference response body for h3 stream: " + streamId());
client().h3StreamUnreference();
}
}
final class Http3PushStreamResponseSubscriber<U> extends HttpBodySubscriberWrapper<U> {
Http3PushStreamResponseSubscriber(BodySubscriber<U> subscriber) {
super(subscriber);
}
@Override
protected void unregister() {
unregisterResponseSubscriber(this);
}
@Override
protected void register() {
registerResponseSubscriber(this);
}
}
Http3PushStreamResponseSubscriber<T> createResponseSubscriber(BodyHandler<T> handler,
ResponseInfo response) {
debug.log("Creating body subscriber");
return new Http3PushStreamResponseSubscriber<>(handler.apply(response));
}
@Override
CompletableFuture<Void> ignoreBody() {
try {
debug.log("Ignoring body");
reader.stream().requestStopSending(Http3Error.H3_REQUEST_CANCELLED.code());
return MinimalFuture.completedFuture(null);
} catch (Throwable e) {
Log.logTrace("Error requesting stop sending for stream {0}: {1}",
streamId(), e.toString());
return MinimalFuture.failedFuture(e);
}
}
@Override
void cancel() {
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() {
}
@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: %s", 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 (responseBodyCF != null) {
responseBodyCF.completeExceptionally(firstError);
}
// will send a RST_STREAM frame
var stream = this.stream;
if (connection.isOpen()) {
if (stream != null) {
if (debug.on())
debug.log("request stop sending");
stream.requestStopSending(error.code());
}
}
} catch (Throwable ex) {
debug.log("failed cancelling request: ", ex);
Log.logError(ex);
} finally {
close();
}
}
@Override
CompletableFuture<Response> getResponseAsync(Executor executor) {
var cf = pushCF;
if (executor != null && !cf.isDone()) {
// protect from executing later chain of CompletableFuture operations from SelectorManager thread
cf = cf.thenApplyAsync(r -> r, executor);
}
Log.logTrace("Response future (stream={0}) is: {1}", streamId(), cf);
if (debug.on()) debug.log("Response future is %s", cf);
return cf;
}
void completeResponse(Response r) {
debug.log("Response: " + r);
Log.logResponse(r::toString);
pushCF.complete(r); // not strictly required for push API
// start reading the body using the obtained BodySubscriber
CompletableFuture<Void> start = new MinimalFuture<>();
start.thenCompose( v -> readBodyAsync(getPushHandler(), false, getExchange().executor()))
.whenComplete((T body, Throwable t) -> {
if (t != null) {
responseCF.completeExceptionally(t);
debug.log("Cancelling push promise %s (stream %s) due to: %s", pushId, streamId(), t);
pushManager.cancelPushPromise(pushId, t, CancelPushReason.PUSH_CANCELLED);
cancelImpl(t, Http3Error.H3_REQUEST_CANCELLED);
} else {
HttpResponseImpl<T> resp =
new HttpResponseImpl<>(r.request, r, null, body, getExchange());
debug.log("Completing responseCF: " + resp);
pushManager.pushPromiseProcessed(pushId);
responseCF.complete(resp);
}
});
start.completeAsync(() -> null, getExchange().executor());
}
// methods to update state and remove stream when finished
void responseReceived() {
stateLock.lock();
try {
responseReceived0();
} finally {
stateLock.unlock();
}
}
private void responseReceived0() {
assert stateLock.isHeldByCurrentThread();
responseReceived = true;
if (debug.on()) debug.log("responseReceived: streamid=%d", streamId());
close();
}
/**
* same as above but for errors
*/
void completeResponseExceptionally(Throwable t) {
pushManager.cancelPushPromise(pushId, t, CancelPushReason.PUSH_CANCELLED);
responseCF.completeExceptionally(t);
}
void nullBody(HttpResponse<T> resp, Throwable t) {
if (debug.on()) debug.log("nullBody: streamid=%d", streamId());
// We should have an END_STREAM data frame waiting in the inputQ.
// We need a subscriber to force the scheduler to process it.
assert pendingResponseSubscriber == null;
pendingResponseSubscriber = HttpResponse.BodySubscribers.replacing(null);
readScheduler.runOrSchedule();
}
@Override
CompletableFuture<ExchangeImpl<T>> sendHeadersAsync() {
return MinimalFuture.completedFuture(this);
}
@Override
CompletableFuture<ExchangeImpl<T>> sendBodyAsync() {
return MinimalFuture.completedFuture(this);
}
CompletableFuture<HttpResponse<T>> responseCF() {
return responseCF;
}
BodyHandler<T> getPushHandler() {
// ignored parameters to function can be used as BodyHandler
return this.pushHandler;
}
@Override
CompletableFuture<T> readBodyAsync(BodyHandler<T> handler,
boolean returnConnectionToPool,
Executor executor) {
try {
Log.logTrace("Reading body on stream {0}", streamId());
debug.log("Getting BodySubscriber for: " + response);
Http3PushStreamResponseSubscriber<T> bodySubscriber =
createResponseSubscriber(handler, new ResponseInfoImpl(response));
CompletableFuture<T> cf = receiveResponseBody(bodySubscriber, executor);
PushGroup<?> pg = parent.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);
PushGroup<?> pg = parent.exchange.getPushGroup();
if (pg != null) {
// if an error occurs make sure it is recorded in the PushGroup
pg.pushError(t);
}
return MinimalFuture.failedFuture(t);
}
}
// This method doesn't send any frame
void close() {
if (closed) return;
stateLock.lock();
try {
if (closed) return;
closed = true;
} finally {
stateLock.unlock();
}
if (debug.on()) debug.log("stream %d is now closed", streamId());
Log.logTrace("Stream {0} is now closed", streamId());
BodySubscriber<T> subscriber = responseSubscriber;
if (subscriber == null) subscriber = pendingResponseSubscriber;
if (subscriber instanceof Http3PushStreamResponseSubscriber<?> h3srs) {
// ensure subscriber is unregistered
h3srs.complete(errorRef.get());
}
connection.onPushPromiseStreamClosed(this, streamId());
}
@Override
Response newResponse(HttpHeaders responseHeaders, int responseCode) {
return this.response = new Response(
exchange.request, exchange, responseHeaders, connection(),
responseCode, Version.HTTP_3);
}
protected void handleResponse() {
handleResponse(respHeadersBuilder, respHeadersConsumer, readScheduler, debug);
}
@Override
void receivePushPromiseFrame(PushPromiseFrame ppf, List<ByteBuffer> payload) throws IOException {
readScheduler.stop();
connectionError(new ProtocolException("Unexpected PUSH_PROMISE on push response stream"), H3_FRAME_UNEXPECTED);
}
@Override
void onPollException(QuicStreamReader reader, IOException io) {
if (Log.http3()) {
Log.logHttp3("{0}/streamId={1} pushId={2} #{3} (responseReceived={4}, " +
"reader={5}, statusCode={6}, finalStream={9}): {10}",
connection().quicConnection().logTag(),
String.valueOf(reader.stream().streamId()), pushId, String.valueOf(exchange.multi.id),
responseReceived, reader.receivingState(),
String.valueOf(responseCode), connection.isFinalStream(), io);
}
}
@Override
void onReaderReset() {
long errorCode = stream.rcvErrorCode();
String resetReason = Http3Error.stringForCode(errorCode);
Http3Error resetError = Http3Error.fromCode(errorCode)
.orElse(Http3Error.H3_REQUEST_CANCELLED);
if (!responseReceived) {
cancelImpl(new IOException("Stream %s reset by peer: %s"
.formatted(streamId(), resetReason)),
resetError);
}
if (debug.on()) {
debug.log("Stream %s reset by peer [%s]: Stopping scheduler",
streamId(), resetReason);
}
readScheduler.stop();
}
// Invoked when some data is received from the request-response
// Quic stream
private void processQuicData() {
// Poll bytes from the request-response stream
// and parses the data to read HTTP/3 frames.
//
// If the frame being read is a header frame, send the
// compacted header field data to QPack.
//
// Otherwise, if it's a data frame, send the bytes
// to the response body subscriber.
//
// Finally, if the frame being read is a PushPromiseFrame,
// sends the compressed field data to the QPack decoder to
// decode the push promise request headers.
try {
processQuicData(reader, framesDecoder, frameOrderVerifier, readScheduler, debug);
} catch (Throwable t) {
debug.log("processQuicData - Unexpected exception", t);
if (!responseReceived) {
cancelImpl(t, Http3Error.H3_REQUEST_CANCELLED);
}
} finally {
debug.log("processQuicData - leaving - eof: %s", framesDecoder.eof());
}
}
// invoked when ByteBuffers containing the next payload bytes for the
// given partial header frame are received
void receiveHeaders(HeadersFrame headers, List<ByteBuffer> payload)
throws IOException {
debug.log("receive headers: buffer list: " + payload);
boolean completed = headers.remaining() == 0;
boolean eof = false;
if (payload != null) {
int last = payload.size() - 1;
for (int i = 0; i <= last; i++) {
ByteBuffer buf = payload.get(i);
boolean endOfHeaders = completed && i == last;
if (debug.on())
debug.log("QPack decoding %s bytes from headers (last: %s)",
buf.remaining(), last);
// if we have finished receiving the header frame, pause reading until
// the status code has been decoded
if (endOfHeaders) switchReadingPaused(true);
qpackDecoder.decodeHeader(buf,
endOfHeaders,
respHeaderFrameReader);
if (buf == QuicStreamReader.EOF) {
// we are at EOF - no need to pause reading
switchReadingPaused(false);
eof = true;
}
}
}
if (!completed && eof) {
cancelImpl(new EOFException("EOF reached: " + headers),
Http3Error.H3_REQUEST_CANCELLED);
}
}
void connectionError(Throwable throwable, long errorCode, String errMsg) {
if (errorRef.compareAndSet(null, throwable)) {
var streamid = streamId();
if (debug.on()) {
if (streamid == -1) {
debug.log("cancelling stream due to connection error", throwable);
} else {
debug.log("cancelling stream " + streamid
+ " due to connection error", throwable);
}
}
if (Log.trace()) {
if (streamid == -1) {
Log.logTrace( "connection error: {0}", errMsg);
} else {
var format = "cancelling stream {0} due to connection error: {1}";
Log.logTrace(format, streamid, errMsg);
}
}
}
connection.connectionError(this, throwable, errorCode, errMsg);
}
// pushes entire response body into response subscriber
// blocking when required by local or remote flow control
CompletableFuture<T> receiveResponseBody(BodySubscriber<T> bodySubscriber, Executor executor) {
// We want to allow the subscriber's getBody() method to block so it
// can work with InputStreams. So, we offload execution.
responseBodyCF = ResponseSubscribers.getBodyAsync(executor, bodySubscriber,
new MinimalFuture<>(), (t) -> this.cancelImpl(t, Http3Error.H3_REQUEST_CANCELLED));
if (isCanceled()) {
Throwable t = getCancelCause();
responseBodyCF.completeExceptionally(t);
}
// ensure that the body subscriber will be subsribed and onError() is
// invoked
pendingResponseSubscriber = bodySubscriber;
readScheduler.runOrSchedule(); // in case data waiting already to be processed, or error
return responseBodyCF;
}
void onSubscriptionError(Throwable t) {
errorRef.compareAndSet(null, t);
if (debug.on()) debug.log("Got subscription error: %s", (Object) t);
// This is the special case where the subscriber
// has requested an illegal number of items.
// In this case, the error doesn't come from
// upstream, but from downstream, and we need to
// handle the error without waiting for the inputQ
// to be exhausted.
stopRequested = true;
readScheduler.runOrSchedule();
}
// This loop is triggered to push response body data into
// the body subscriber.
void pushResponseData(ConcurrentLinkedQueue<List<ByteBuffer>> responseData) {
debug.log("pushResponseData");
boolean onCompleteCalled = false;
BodySubscriber<T> subscriber = responseSubscriber;
boolean done = false;
try {
if (subscriber == null) {
subscriber = responseSubscriber = pendingResponseSubscriber;
if (subscriber == null) {
// can't process anything yet
return;
} else {
if (debug.on()) debug.log("subscribing user subscriber");
subscriber.onSubscribe(userSubscription);
}
}
while (!responseData.isEmpty()) {
List<ByteBuffer> data = responseData.peek();
List<ByteBuffer> dsts = Collections.unmodifiableList(data);
long size = Utils.remaining(dsts, Long.MAX_VALUE);
boolean finished = dsts.contains(QuicStreamReader.EOF);
if (size == 0 && finished) {
responseData.remove();
Log.logTrace("responseSubscriber.onComplete");
if (debug.on()) debug.log("pushResponseData: onComplete");
subscriber.onComplete();
done = true;
onCompleteCalled = true;
responseReceived();
return;
} else if (userSubscription.tryDecrement()) {
responseData.remove();
Log.logTrace("responseSubscriber.onNext {0}", size);
if (debug.on()) debug.log("pushResponseData: onNext(%d)", size);
subscriber.onNext(dsts);
} else {
if (stopRequested) break;
debug.log("no demand");
return;
}
}
if (framesDecoder.eof() && responseData.isEmpty()) {
debug.log("pushResponseData: EOF");
if (!onCompleteCalled) {
Log.logTrace("responseSubscriber.onComplete");
if (debug.on()) debug.log("pushResponseData: onComplete");
subscriber.onComplete();
done = true;
onCompleteCalled = true;
responseReceived();
return;
}
}
} catch (Throwable throwable) {
debug.log("pushResponseData: unexpected exception", throwable);
errorRef.compareAndSet(null, throwable);
} finally {
if (done) responseData.clear();
}
Throwable t = errorRef.get();
if (t != null) {
try {
if (!onCompleteCalled) {
if (debug.on())
debug.log("calling subscriber.onError: %s", (Object) t);
subscriber.onError(t);
} else {
if (debug.on())
debug.log("already completed: dropping error %s", (Object) t);
}
} catch (Throwable x) {
Log.logError("Subscriber::onError threw exception: {0}", t);
} finally {
cancelImpl(t, Http3Error.H3_REQUEST_CANCELLED);
responseData.clear();
}
}
}
}

View File

@ -0,0 +1,693 @@
/*
* 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.http;
import java.io.EOFException;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.ProtocolException;
import java.net.http.HttpHeaders;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.OptionalLong;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;
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.SequentialScheduler;
import jdk.internal.net.http.common.Utils;
import jdk.internal.net.http.common.ValidatingHeadersConsumer;
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.Http3Frame;
import jdk.internal.net.http.http3.frames.Http3FrameType;
import jdk.internal.net.http.http3.frames.MalformedFrame;
import jdk.internal.net.http.http3.frames.PartialFrame;
import jdk.internal.net.http.http3.frames.PushPromiseFrame;
import jdk.internal.net.http.http3.frames.UnknownFrame;
import jdk.internal.net.http.qpack.Decoder;
import jdk.internal.net.http.qpack.DecodingCallback;
import jdk.internal.net.http.qpack.readers.HeaderFrameReader;
import jdk.internal.net.http.quic.streams.QuicStreamReader;
import static jdk.internal.net.http.Exchange.MAX_NON_FINAL_RESPONSES;
import static jdk.internal.net.http.RedirectFilter.HTTP_NOT_MODIFIED;
/**
* A common super class for the HTTP/3 request/response stream ({@link Http3ExchangeImpl}
* and the HTTP/3 push promises stream ({@link Http3PushPromiseStream}.
* @param <T> the expected type of the response body
*/
sealed abstract class Http3Stream<T> extends ExchangeImpl<T> permits Http3ExchangeImpl, Http3PushPromiseStream {
enum ResponseState { PERMIT_HEADER, PERMIT_TRAILER, PERMIT_NONE }
// count of bytes read from the Quic stream. This is weakly consistent and
// used for debug only. Must not be updated outside of processQuicData
private volatile long receivedQuicBytes;
// keep track of which HTTP/3 frames have been parsed and whether more header
// frames are permitted
private ResponseState responseState = ResponseState.PERMIT_HEADER;
// value of content-length header in the response header, or null
private Long contentLength;
// number of data bytes delivered to user subscriber
private long consumedDataBytes;
// switched to true if reading from the quic stream should be temporarily
// paused. After switching back to false, readScheduler.runOrSchedule() should
// called.
private volatile boolean readingPaused;
// A temporary buffer for response body bytes
final ConcurrentLinkedQueue<List<ByteBuffer>> responseData = new ConcurrentLinkedQueue<>();
private final AtomicInteger nonFinalResponseCount = new AtomicInteger();
Http3Stream(Exchange<T> exchange) {
super(exchange);
}
/**
* Cancel the stream exchange on error
* @param throwable an exception to be relayed to the multi exchange
* through the completable future chain
* @param error an HTTP/3 error
*/
abstract void cancelImpl(Throwable throwable, Http3Error error);
/**
* {@return the Quic stream id for this exchange (request/response or push response)}
*/
abstract long streamId();
/**
* A base class implementing {@link DecodingCallback} used for receiving
* and building HttpHeaders. Can be used for request headers, response headers,
* push response headers, or trailers.
*/
abstract class StreamHeadersConsumer extends ValidatingHeadersConsumer
implements DecodingCallback {
private volatile boolean hasError;
StreamHeadersConsumer(Context context) {
super(context);
}
abstract Decoder qpackDecoder();
abstract HeaderFrameReader headerFrameReader();
abstract HttpHeadersBuilder headersBuilder();
abstract void resetDone();
@Override
public void reset() {
super.reset();
headerFrameReader().reset();
headersBuilder().clear();
hasError = false;
resetDone();
}
String headerFieldType() {return "HEADER FIELD";}
@Override
public void onDecoded(CharSequence name, CharSequence value) {
try {
String n = name.toString();
String v = value.toString();
super.onDecoded(n, v);
headersBuilder().addHeader(n, v);
if (Log.headers() && Log.trace()) {
Log.logTrace("RECEIVED {0} (streamid={1}): {2}: {3}",
headerFieldType(), streamId(), n, v);
}
} catch (Throwable throwable) {
if (throwable instanceof UncheckedIOException uio) {
// UncheckedIOException is thrown by ValidatingHeadersConsumer.onDecoded
// for cases with invalid headers or unknown/unsupported pseudo-headers.
// It should be treated as a malformed request.
// RFC-9114 4.1.2. Malformed Requests and Responses:
// Malformed requests or responses that are
// detected MUST be treated as a stream error of
// type H3_MESSAGE_ERROR.
onStreamError(uio.getCause(), Http3Error.H3_MESSAGE_ERROR);
} else {
onConnectionError(throwable, Http3Error.H3_INTERNAL_ERROR);
}
}
}
@Override
public void onComplete() {
// RFC-9204 2.2.2.1: After the decoder finishes decoding a field
// section encoded using representations containing dynamic table
// references, it MUST emit a Section Acknowledgment instruction
qpackDecoder().ackSection(streamId(), headerFrameReader());
qpackDecoder().resetInsertionsCounter();
headersCompleted();
}
abstract void headersCompleted();
@Override
public void onStreamError(Throwable throwable, Http3Error http3Error) {
hasError = true;
qpackDecoder().resetInsertionsCounter();
// Stream error
cancelImpl(throwable, http3Error);
}
@Override
public void onConnectionError(Throwable throwable, Http3Error http3Error) {
hasError = true;
// Connection error
connectionError(throwable, http3Error);
}
@Override
public boolean hasError() {
return hasError;
}
}
/**
* {@return count of bytes read from the QUIC stream so far}
*/
public long receivedQuicBytes() {
return receivedQuicBytes;
}
/**
* Notify of a connection error.
*
* The implementation of this method is supposed to close all
* exchanges, cancel all push promises, and close the connection.
*
* @implSpec
* The implementation of this method calls
* {@snippet lang=java :
* connectionError(throwable, error.code(), throwable.getMessage());
* }
*
* @param throwable an exception to be relayed to the multi exchange
* through the completable future chain
* @param error an HTTP/3 error
*/
void connectionError(Throwable throwable, Http3Error error) {
connectionError(throwable, error.code(), throwable.getMessage());
}
/**
* Notify of a connection error.
*
* The implementation of this method is supposed to close all
* exchanges, cancel all push promises, and close the connection.
*
* @param throwable an exception to be relayed to the multi exchange
* through the completable future chain
* @param errorCode an HTTP/3 error code
* @param errMsg an error message to be logged when closing the connection
*/
abstract void connectionError(Throwable throwable, long errorCode, String errMsg);
/**
* Push response data to the {@linkplain java.net.http.HttpResponse.BodySubscriber
* response body subscriber} if allowed by the subscription state.
* @param responseData a queue of available data to be pushed to the subscriber
*/
abstract void pushResponseData(ConcurrentLinkedQueue<List<ByteBuffer>> responseData);
/**
* Called when an exception is thrown by {@link QuicStreamReader#poll() reader::poll}
* when called from {@link #processQuicData(QuicStreamReader, FramesDecoder,
* H3FrameOrderVerifier, SequentialScheduler, Logger) processQuicData}.
* This is typically only used for logging purposes.
* @param reader the stream reader
* @param io the exception caught
*/
abstract void onPollException(QuicStreamReader reader, IOException io);
/**
* Called when new payload data is received by {@link #processQuicData(QuicStreamReader,
* FramesDecoder, H3FrameOrderVerifier, SequentialScheduler, Logger) processQuicData}
* for a given header frame.
* <p>
* Any exception thrown here will be rethrown by {@code processQuicData}
*
* @param headers a partially received header frame
* @param payload the payload bytes available for that frame
* @throws IOException if an error is detected
*/
abstract void receiveHeaders(HeadersFrame headers, List<ByteBuffer> payload) throws IOException;
/**
* Called when new payload data is received by {@link #processQuicData(QuicStreamReader,
* FramesDecoder, H3FrameOrderVerifier, SequentialScheduler, Logger) processQuicData}
* for a given push promise frame.
* <p>
* Any exception thrown here will be rethrown by {@code processQuicData}
*
* @param ppf a partially received push promise frame
* @param payload the payload bytes available for that frame
* @throws IOException if an error is detected
*/
abstract void receivePushPromiseFrame(PushPromiseFrame ppf, List<ByteBuffer> payload) throws IOException;
/**
* {@return whether reading from the quic stream is currently paused}
* Typically reading is paused when waiting for headers to be decoded by QPack.
*/
boolean readingPaused() {return readingPaused;}
/**
* Switches the value of the {@link #readingPaused() readingPaused}
* flag
* <p>
* Subclasses of {@code Http3Stream} can call this method to switch
* the value of this flag if needed, typically in their
* concrete implementation of {@link #receiveHeaders(HeadersFrame, List)}.
* @param value the new value
*/
void switchReadingPaused(boolean value) {
readingPaused = value;
}
// invoked when ByteBuffers containing the next payload bytes for the
// given partial data frame are received.
private void receiveData(DataFrame data, List<ByteBuffer> payload, Logger debug) {
if (debug.on()) {
debug.log("receiveData: adding %s payload byte", Utils.remaining(payload));
}
responseData.add(payload);
pushResponseData(responseData);
}
private ByteBuffer pollIfNotReset(QuicStreamReader reader) throws IOException {
ByteBuffer buffer;
try {
if (reader.isReset()) return null;
buffer = reader.poll();
} catch (IOException io) {
if (reader.isReset()) return null;
onPollException(reader, io);
throw io;
}
return buffer;
}
private Throwable toThrowable(MalformedFrame malformedFrame) {
Throwable cause = malformedFrame.getCause();
if (cause != null) return cause;
return new ProtocolException(malformedFrame.toString());
}
/**
* Called when {@code processQuicData} detects that the {@linkplain
* QuicStreamReader reader} has been reset.
* This method should do the appropriate garbage collection,
* possibly closing the exchange or the connection if needed, and
* closing the read scheduler.
*/
abstract void onReaderReset();
/**
* Invoked when some data is received from the underlying quic stream.
* This implements the read loop for a request-response stream or a
* push response stream.
*/
void processQuicData(QuicStreamReader reader,
FramesDecoder framesDecoder,
H3FrameOrderVerifier frameOrderVerifier,
SequentialScheduler readScheduler,
Logger debug) throws IOException {
// Poll bytes from the request-response stream
// and parses the data to read HTTP/3 frames.
//
// If the frame being read is a header frame, send the
// compacted header field data to QPack.
//
// Otherwise, if it's a data frame, send the bytes
// to the response body subscriber.
//
// Finally, if the frame being read is a PushPromiseFrame,
// sends the compressed field data to the QPack decoder to
// decode the push promise request headers.
//
// the reader might be null if the loop is triggered before
// the field is assigned
if (reader == null) return;
// check whether we need to wait until response headers
// have been decoded: in that case readingPaused will be true
if (readingPaused) return;
if (debug.on()) debug.log("processQuicData");
ByteBuffer buffer;
Http3Frame frame;
pushResponseData(responseData);
boolean readmore = responseData.isEmpty();
// do not read more until data has been pulled
while (readmore && (buffer = pollIfNotReset(reader)) != null) {
if (debug.on())
debug.log("processQuicData - submitting buffer: %s bytes (ByteBuffer@%s)",
buffer.remaining(), System.identityHashCode(buffer));
// only updated here
var received = receivedQuicBytes;
receivedQuicBytes = received + buffer.remaining();
framesDecoder.submit(buffer);
while ((frame = framesDecoder.poll()) != null) {
if (debug.on()) debug.log("processQuicData - frame: " + frame);
final long frameType = frame.type();
// before we start processing, verify that this frame *type* has arrived in the
// allowed order
if (!frameOrderVerifier.allowsProcessing(frame)) {
final String unexpectedFrameType = Http3FrameType.asString(frameType);
// not expected to be arriving now
// RFC-9114, section 4.1 - Receipt of an invalid sequence of frames MUST be
// treated as a connection error of type H3_FRAME_UNEXPECTED.
if (debug.on()) {
debug.log("unexpected (order of) frame type: "
+ unexpectedFrameType + " on stream");
}
Log.logError("Connection error due to unexpected (order of) frame type" +
" {0} on stream", unexpectedFrameType);
readScheduler.stop();
final String errMsg = "Unexpected frame " + unexpectedFrameType;
connectionError(new ProtocolException(errMsg), Http3Error.H3_FRAME_UNEXPECTED);
return;
}
if (frame instanceof PartialFrame partialFrame) {
final List<ByteBuffer> payload = framesDecoder.readPayloadBytes();
if (debug.on()) {
debug.log("processQuicData - payload: %s",
payload == null ? null : Utils.remaining(payload));
}
if (framesDecoder.eof() && !framesDecoder.clean()) {
String msg = "Frame truncated: " + partialFrame;
connectionError(new ProtocolException(msg),
Http3Error.H3_FRAME_ERROR.code(),
msg);
break;
}
if ((payload == null || payload.isEmpty()) && partialFrame.remaining() != 0) {
break;
}
if (partialFrame instanceof HeadersFrame headers) {
receiveHeaders(headers, payload);
// check if we need to wait for the status code to be decoded
// before reading more
readmore = !readingPaused;
} else if (partialFrame instanceof DataFrame data) {
if (responseState != ResponseState.PERMIT_TRAILER) {
cancelImpl(new IOException("DATA frame not expected here"), Http3Error.H3_MESSAGE_ERROR);
return;
}
if (payload != null) {
consumedDataBytes += Utils.remaining(payload);
if (contentLength != null &&
consumedDataBytes + data.remaining() > contentLength) {
cancelImpl(new IOException(
String.format("DATA frame (length %d) exceeds content-length (%d) by %d",
data.streamingLength(), contentLength,
consumedDataBytes + data.remaining() - contentLength)),
Http3Error.H3_MESSAGE_ERROR);
return;
}
// don't read more if there is pending data waiting
// to be read from downstream
readmore = responseData.isEmpty();
receiveData(data, payload, debug);
}
} else if (partialFrame instanceof PushPromiseFrame ppf) {
receivePushPromiseFrame(ppf, payload);
} else if (partialFrame instanceof UnknownFrame) {
if (debug.on()) {
debug.log("ignoring %s bytes for unknown frame type: %s",
Utils.remaining(payload),
Http3FrameType.asString(frameType));
}
} else {
// should never come here: the only frame that we can
// receive on a request-response stream are
// HEADERS, DATA, PUSH_PROMISE, and RESERVED/UNKNOWN
// All have already been taken care above.
// So this here should be dead-code.
String msg = "unhandled frame type: " +
Http3FrameType.asString(frameType);
if (debug.on()) debug.log("Warning: %s", msg);
throw new AssertionError(msg);
}
// mark as complete, if all expected data has been read for a frame
if (partialFrame.remaining() == 0) {
frameOrderVerifier.completed(frame);
}
} else if (frame instanceof MalformedFrame malformed) {
var cause = malformed.getCause();
if (cause != null && debug.on()) {
debug.log(malformed.toString(), cause);
}
readScheduler.stop();
connectionError(toThrowable(malformed),
malformed.getErrorCode(),
malformed.getMessage());
return;
} else {
// should never come here: the only frame that we can
// receive on a request-response stream are
// HEADERS, DATA, PUSH_PROMISE, and RESERVED/UNKNOWN
// All should have already been taken care above,
// including malformed frames. So this here should be
// dead-code.
String msg = "unhandled frame type: " +
Http3FrameType.asString(frameType);
if (debug.on()) debug.log("Warning: %s", msg);
throw new AssertionError(msg);
}
if (framesDecoder.eof()) break;
}
if (framesDecoder.eof()) break;
}
if (framesDecoder.eof()) {
if (!framesDecoder.clean()) {
String msg = "EOF reading frame type and length";
connectionError(new ProtocolException(msg),
Http3Error.H3_FRAME_ERROR.code(),
msg);
}
if (debug.on()) debug.log("processQuicData - EOF");
if (responseState == ResponseState.PERMIT_HEADER) {
cancelImpl(new EOFException("EOF reached: no header bytes received"), Http3Error.H3_MESSAGE_ERROR);
} else {
if (contentLength != null &&
consumedDataBytes != contentLength) {
cancelImpl(new IOException(
String.format("fixed content-length: %d, bytes received: %d", contentLength, consumedDataBytes)),
Http3Error.H3_MESSAGE_ERROR);
return;
}
receiveData(new DataFrame(0),
List.of(QuicStreamReader.EOF), debug);
}
}
if (framesDecoder.eof() && responseData.isEmpty()) {
if (debug.on()) debug.log("EOF: Stopping scheduler");
readScheduler.stop();
}
if (reader.isReset() && responseData.isEmpty()) {
onReaderReset();
}
}
final String checkInterimResponseCountExceeded() {
// this is also checked by Exchange - but tracking it here too provides
// a more informative message.
int count = nonFinalResponseCount.incrementAndGet();
if (MAX_NON_FINAL_RESPONSES > 0 && (count < 0 || count > MAX_NON_FINAL_RESPONSES)) {
return String.format(
"Stream %s PROTOCOL_ERROR: too many interim responses received: %s > %s",
streamId(), count, MAX_NON_FINAL_RESPONSES);
}
return null;
}
/**
* Called to create a new Response object for the newly receive response headers and
* response status code. This method is called from {@link #handleResponse(HttpHeadersBuilder,
* StreamHeadersConsumer, SequentialScheduler, Logger) handleResponse}, after the status code
* and headers have been validated.
*
* @param responseHeaders response headers
* @param responseCode response code
* @return a new {@code Response} object
*/
abstract Response newResponse(HttpHeaders responseHeaders, int responseCode);
/**
* Called at the end of {@link #handleResponse(HttpHeadersBuilder,
* StreamHeadersConsumer, SequentialScheduler, Logger) handleResponse}, to propagate
* the response to the multi exchange.
* @param response the {@code Response} that was received.
*/
abstract void completeResponse(Response response);
/**
* Validate response headers and status code based on the {@link #responseState}.
* If validated, this method will call {@link #newResponse(HttpHeaders, int)} to
* create a {@code Response} object, which it will then pass to
* {@link #completeResponse(Response)}.
*
* @param responseHeadersBuilder the response headers builder
* @param rspHeadersConsumer the response headers consumer
* @param readScheduler the read scheduler
* @param debug the debug logger
*/
void handleResponse(HttpHeadersBuilder responseHeadersBuilder,
StreamHeadersConsumer rspHeadersConsumer,
SequentialScheduler readScheduler,
Logger debug) {
if (responseState == ResponseState.PERMIT_NONE) {
connectionError(new ProtocolException("HEADERS after trailer"),
Http3Error.H3_FRAME_UNEXPECTED.code(),
"HEADERS after trailer");
return;
}
HttpHeaders responseHeaders = responseHeadersBuilder.build();
if (responseState == ResponseState.PERMIT_TRAILER) {
if (responseHeaders.firstValue(":status").isPresent()) {
cancelImpl(new IOException("Unexpected :status header in trailer"), Http3Error.H3_MESSAGE_ERROR);
return;
}
if (Log.headers()) {
Log.logHeaders("Ignoring trailers on stream {0}: {1}", streamId(), responseHeaders);
} else if (debug.on()) {
debug.log("Ignoring trailers: %s", responseHeaders);
}
responseState = ResponseState.PERMIT_NONE;
rspHeadersConsumer.reset();
if (readingPaused) {
readingPaused = false;
readScheduler.runOrSchedule(exchange.executor());
}
return;
}
int responseCode;
boolean finalResponse = false;
try {
responseCode = (int) responseHeaders
.firstValueAsLong(":status")
.orElseThrow(() -> new IOException("no statuscode in response"));
} catch (IOException | NumberFormatException exception) {
// RFC-9114: 4.1.2. Malformed Requests and Responses:
// "Malformed requests or responses that are
// detected MUST be treated as a stream error of type H3_MESSAGE_ERROR"
cancelImpl(exception, Http3Error.H3_MESSAGE_ERROR);
return;
}
if (responseCode < 100 || responseCode > 999) {
cancelImpl(new IOException("Unexpected :status header value"), Http3Error.H3_MESSAGE_ERROR);
return;
}
if (responseCode >= 200) {
responseState = ResponseState.PERMIT_TRAILER;
finalResponse = true;
} else {
assert responseCode >= 100 && responseCode <= 200 : "unexpected responseCode: " + responseCode;
String protocolErrorMsg = checkInterimResponseCountExceeded();
if (protocolErrorMsg != null) {
if (debug.on()) {
debug.log(protocolErrorMsg);
}
cancelImpl(new ProtocolException(protocolErrorMsg), Http3Error.H3_GENERAL_PROTOCOL_ERROR);
rspHeadersConsumer.reset();
return;
}
}
// update readingPaused after having decoded the statusCode and
// switched the responseState.
if (readingPaused) {
readingPaused = false;
readScheduler.runOrSchedule(exchange.executor());
}
var response = newResponse(responseHeaders, responseCode);
if (debug.on()) {
debug.log("received response headers: %s",
responseHeaders);
}
try {
OptionalLong cl = responseHeaders.firstValueAsLong("content-length");
if (finalResponse && cl.isPresent()) {
long cll = cl.getAsLong();
if (cll < 0) {
cancelImpl(new IOException("Invalid content-length value "+cll), Http3Error.H3_MESSAGE_ERROR);
return;
}
if (!(exchange.request().method().equalsIgnoreCase("HEAD") || responseCode == HTTP_NOT_MODIFIED)) {
// HEAD response and 304 response might have a content-length header,
// but it carries no meaning
contentLength = cll;
}
}
} catch (NumberFormatException nfe) {
cancelImpl(nfe, Http3Error.H3_MESSAGE_ERROR);
return;
}
if (Log.headers() || debug.on()) {
StringBuilder sb = new StringBuilder("H3 RESPONSE HEADERS (stream=");
sb.append(streamId()).append(")\n");
Log.dumpHeaders(sb, " ", responseHeaders);
if (Log.headers()) {
Log.logHeaders(sb.toString());
} else if (debug.on()) {
debug.log(sb);
}
}
// this will clear the response headers
rspHeadersConsumer.reset();
completeResponse(response);
}
}

View File

@ -41,6 +41,7 @@ import java.net.ProtocolException;
import java.net.ProxySelector;
import java.net.http.HttpConnectTimeoutException;
import java.net.http.HttpTimeoutException;
import java.net.http.UnsupportedProtocolVersionException;
import java.nio.ByteBuffer;
import java.nio.channels.CancelledKeyException;
import java.nio.channels.ClosedChannelException;
@ -93,8 +94,16 @@ import jdk.internal.net.http.common.TimeSource;
import jdk.internal.net.http.common.Utils;
import jdk.internal.net.http.common.OperationTrackers.Trackable;
import jdk.internal.net.http.common.OperationTrackers.Tracker;
import jdk.internal.net.http.common.Utils.SafeExecutor;
import jdk.internal.net.http.common.Utils.SafeExecutorService;
import jdk.internal.net.http.websocket.BuilderImpl;
import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY;
import static java.net.http.HttpOption.H3_DISCOVERY;
import static java.util.Objects.requireNonNullElse;
import static java.util.Objects.requireNonNullElseGet;
import static jdk.internal.net.quic.QuicTLSContext.isQuicCompatible;
/**
* Client implementation. Contains all configuration information and also
* the selector manager thread which allows async events to be registered
@ -112,7 +121,8 @@ final class HttpClientImpl extends HttpClient implements Trackable {
static final int DEFAULT_KEEP_ALIVE_TIMEOUT = 30;
static final long KEEP_ALIVE_TIMEOUT = getTimeoutProp("jdk.httpclient.keepalive.timeout", DEFAULT_KEEP_ALIVE_TIMEOUT);
// Defaults to value used for HTTP/1 Keep-Alive Timeout. Can be overridden by jdk.httpclient.keepalive.timeout.h2 property.
static final long IDLE_CONNECTION_TIMEOUT = getTimeoutProp("jdk.httpclient.keepalive.timeout.h2", KEEP_ALIVE_TIMEOUT);
static final long IDLE_CONNECTION_TIMEOUT_H2 = getTimeoutProp("jdk.httpclient.keepalive.timeout.h2", KEEP_ALIVE_TIMEOUT);
static final long IDLE_CONNECTION_TIMEOUT_H3 = getTimeoutProp("jdk.httpclient.keepalive.timeout.h3", IDLE_CONNECTION_TIMEOUT_H2);
// Define the default factory as a static inner class
// that embeds all the necessary logic to avoid
@ -145,15 +155,23 @@ final class HttpClientImpl extends HttpClient implements Trackable {
static final class DelegatingExecutor implements Executor {
private final BooleanSupplier isInSelectorThread;
private final Executor delegate;
private final SafeExecutor<?> safeDelegate;
private final BiConsumer<Runnable, Throwable> errorHandler;
DelegatingExecutor(BooleanSupplier isInSelectorThread,
Executor delegate,
BiConsumer<Runnable, Throwable> errorHandler) {
this.isInSelectorThread = isInSelectorThread;
this.delegate = delegate;
this.safeDelegate = delegate instanceof ExecutorService svc
? new SafeExecutorService(svc, ASYNC_POOL, errorHandler)
: new SafeExecutor<>(delegate, ASYNC_POOL, errorHandler);
this.errorHandler = errorHandler;
}
Executor safeDelegate() {
return safeDelegate;
}
Executor delegate() {
return delegate;
}
@ -325,6 +343,8 @@ final class HttpClientImpl extends HttpClient implements Trackable {
private final SelectorManager selmgr;
private final FilterFactory filters;
private final Http2ClientImpl client2;
private final Http3ClientImpl client3;
private final AltServicesRegistry registry;
private final long id;
private final String dbgTag;
private final InetAddress localAddr;
@ -386,6 +406,7 @@ final class HttpClientImpl extends HttpClient implements Trackable {
private final AtomicLong pendingHttpOperationsCount = new AtomicLong();
private final AtomicLong pendingHttpRequestCount = new AtomicLong();
private final AtomicLong pendingHttp2StreamCount = new AtomicLong();
private final AtomicLong pendingHttp3StreamCount = new AtomicLong();
private final AtomicLong pendingTCPConnectionCount = new AtomicLong();
private final AtomicLong pendingSubscribersCount = new AtomicLong();
private final AtomicBoolean isAlive = new AtomicBoolean();
@ -429,14 +450,26 @@ final class HttpClientImpl extends HttpClient implements Trackable {
id = CLIENT_IDS.incrementAndGet();
dbgTag = "HttpClientImpl(" + id +")";
localAddr = builder.localAddr;
if (builder.sslContext == null) {
version = requireNonNullElse(builder.version, Version.HTTP_2);
sslContext = requireNonNullElseGet(builder.sslContext, () -> {
try {
sslContext = SSLContext.getDefault();
return SSLContext.getDefault();
} catch (NoSuchAlgorithmException ex) {
throw new UncheckedIOException(new IOException(ex));
}
} else {
sslContext = builder.sslContext;
});
final boolean sslCtxSupportedForH3 = isQuicCompatible(sslContext);
if (version == Version.HTTP_3 && !sslCtxSupportedForH3) {
throw new UncheckedIOException(new UnsupportedProtocolVersionException(
"HTTP3 is not supported"));
}
sslParams = requireNonNullElseGet(builder.sslParams, sslContext::getDefaultSSLParameters);
boolean sslParamsSupportedForH3 = sslParams.getProtocols() == null
|| sslParams.getProtocols().length == 0
|| isQuicCompatible(sslParams);
if (version == Version.HTTP_3 && !sslParamsSupportedForH3) {
throw new UncheckedIOException(new UnsupportedProtocolVersionException(
"HTTP3 is not supported - TLSv1.3 isn't configured on SSLParameters"));
}
Executor ex = builder.executor;
if (ex == null) {
@ -450,7 +483,6 @@ final class HttpClientImpl extends HttpClient implements Trackable {
this::onSubmitFailure);
facadeRef = new WeakReference<>(facadeFactory.createFacade(this));
implRef = new WeakReference<>(this);
client2 = new Http2ClientImpl(this);
cookieHandler = builder.cookieHandler;
connectTimeout = builder.connectTimeout;
followRedirects = builder.followRedirects == null ?
@ -462,17 +494,11 @@ final class HttpClientImpl extends HttpClient implements Trackable {
debug.log("proxySelector is %s (user-supplied=%s)",
this.proxySelector, userProxySelector != null);
authenticator = builder.authenticator;
if (builder.version == null) {
version = HttpClient.Version.HTTP_2;
} else {
version = builder.version;
}
if (builder.sslParams == null) {
sslParams = getDefaultParams(sslContext);
} else {
sslParams = builder.sslParams;
}
boolean h3Supported = sslCtxSupportedForH3 && sslParamsSupportedForH3;
registry = new AltServicesRegistry(id);
connections = new ConnectionPool(id);
client2 = new Http2ClientImpl(this);
client3 = h3Supported ? new Http3ClientImpl(this) : null;
connections.start();
timeouts = new TreeSet<>();
try {
@ -518,6 +544,11 @@ final class HttpClientImpl extends HttpClient implements Trackable {
client2.stop();
// make sure all subscribers are completed
closeSubscribers();
// close client3
if (client3 != null) {
// close client3
client3.stop();
}
// close TCP connection if any are still opened
openedConnections.forEach(this::closeConnection);
// shutdown the executor if needed
@ -610,11 +641,6 @@ final class HttpClientImpl extends HttpClient implements Trackable {
return isStarted.get() && !isAlive.get();
}
private static SSLParameters getDefaultParams(SSLContext ctx) {
SSLParameters params = ctx.getDefaultSSLParameters();
return params;
}
// Returns the facade that was returned to the application code.
// May be null if that facade is no longer referenced.
final HttpClientFacade facade() {
@ -664,12 +690,14 @@ final class HttpClientImpl extends HttpClient implements Trackable {
final long count = pendingOperationCount.decrementAndGet();
final long httpCount = pendingHttpOperationsCount.decrementAndGet();
final long http2Count = pendingHttp2StreamCount.get();
final long http3Count = pendingHttp3StreamCount.get();
final long webSocketCount = pendingWebSocketCount.get();
if (count == 0 && (facadeRef.refersTo(null) || shutdownRequested)) {
selmgr.wakeupSelector();
}
assert httpCount >= 0 : "count of HTTP/1.1 operations < 0";
assert http2Count >= 0 : "count of HTTP/2 operations < 0";
assert http3Count >= 0 : "count of HTTP/3 operations < 0";
assert webSocketCount >= 0 : "count of WS operations < 0";
assert count >= 0 : "count of pending operations < 0";
return count;
@ -681,10 +709,35 @@ final class HttpClientImpl extends HttpClient implements Trackable {
return pendingOperationCount.incrementAndGet();
}
// Increments the pendingHttp3StreamCount and pendingOperationCount.
final long h3StreamReference() {
pendingHttp3StreamCount.incrementAndGet();
return pendingOperationCount.incrementAndGet();
}
// Decrements the pendingHttp2StreamCount and pendingOperationCount.
final long streamUnreference() {
final long count = pendingOperationCount.decrementAndGet();
final long http2Count = pendingHttp2StreamCount.decrementAndGet();
final long http3Count = pendingHttp3StreamCount.get();
final long httpCount = pendingHttpOperationsCount.get();
final long webSocketCount = pendingWebSocketCount.get();
if (count == 0 && facadeRef.refersTo(null)) {
selmgr.wakeupSelector();
}
assert httpCount >= 0 : "count of HTTP/1.1 operations < 0";
assert http2Count >= 0 : "count of HTTP/2 operations < 0";
assert http3Count >= 0 : "count of HTTP/3 operations < 0";
assert webSocketCount >= 0 : "count of WS operations < 0";
assert count >= 0 : "count of pending operations < 0";
return count;
}
// Decrements the pendingHttp3StreamCount and pendingOperationCount.
final long h3StreamUnreference() {
final long count = pendingOperationCount.decrementAndGet();
final long http2Count = pendingHttp2StreamCount.get();
final long http3Count = pendingHttp3StreamCount.decrementAndGet();
final long httpCount = pendingHttpOperationsCount.get();
final long webSocketCount = pendingWebSocketCount.get();
if (count == 0 && (facadeRef.refersTo(null) || shutdownRequested)) {
@ -692,6 +745,7 @@ final class HttpClientImpl extends HttpClient implements Trackable {
}
assert httpCount >= 0 : "count of HTTP/1.1 operations < 0";
assert http2Count >= 0 : "count of HTTP/2 operations < 0";
assert http3Count >= 0 : "count of HTTP/3 operations < 0";
assert webSocketCount >= 0 : "count of WS operations < 0";
assert count >= 0 : "count of pending operations < 0";
return count;
@ -709,11 +763,13 @@ final class HttpClientImpl extends HttpClient implements Trackable {
final long webSocketCount = pendingWebSocketCount.decrementAndGet();
final long httpCount = pendingHttpOperationsCount.get();
final long http2Count = pendingHttp2StreamCount.get();
final long http3Count = pendingHttp3StreamCount.get();
if (count == 0 && (facadeRef.refersTo(null) || shutdownRequested)) {
selmgr.wakeupSelector();
}
assert httpCount >= 0 : "count of HTTP/1.1 operations < 0";
assert http2Count >= 0 : "count of HTTP/2 operations < 0";
assert http3Count >= 0 : "count of HTTP/3 operations < 0";
assert webSocketCount >= 0 : "count of WS operations < 0";
assert count >= 0 : "count of pending operations < 0";
return count;
@ -732,6 +788,7 @@ final class HttpClientImpl extends HttpClient implements Trackable {
final AtomicLong requestCount;
final AtomicLong httpCount;
final AtomicLong http2Count;
final AtomicLong http3Count;
final AtomicLong websocketCount;
final AtomicLong operationsCount;
final AtomicLong connnectionsCount;
@ -744,6 +801,7 @@ final class HttpClientImpl extends HttpClient implements Trackable {
HttpClientTracker(AtomicLong request,
AtomicLong http,
AtomicLong http2,
AtomicLong http3,
AtomicLong ws,
AtomicLong ops,
AtomicLong conns,
@ -756,6 +814,7 @@ final class HttpClientImpl extends HttpClient implements Trackable {
this.requestCount = request;
this.httpCount = http;
this.http2Count = http2;
this.http3Count = http3;
this.websocketCount = ws;
this.operationsCount = ops;
this.connnectionsCount = conns;
@ -787,6 +846,8 @@ final class HttpClientImpl extends HttpClient implements Trackable {
@Override
public long getOutstandingHttp2Streams() { return http2Count.get(); }
@Override
public long getOutstandingHttp3Streams() { return http3Count.get(); }
@Override
public long getOutstandingWebSocketOperations() {
return websocketCount.get();
}
@ -811,6 +872,7 @@ final class HttpClientImpl extends HttpClient implements Trackable {
pendingHttpRequestCount,
pendingHttpOperationsCount,
pendingHttp2StreamCount,
pendingHttp3StreamCount,
pendingWebSocketCount,
pendingOperationCount,
pendingTCPConnectionCount,
@ -866,6 +928,8 @@ final class HttpClientImpl extends HttpClient implements Trackable {
return Thread.currentThread() == selmgr;
}
AltServicesRegistry registry() { return registry; }
boolean isSelectorClosed() {
return selmgr.isClosed();
}
@ -878,6 +942,10 @@ final class HttpClientImpl extends HttpClient implements Trackable {
return client2;
}
Optional<Http3ClientImpl> client3() {
return Optional.ofNullable(client3);
}
private void debugCompleted(String tag, long startNanos, HttpRequest req) {
if (debugelapsed.on()) {
debugelapsed.log(tag + " elapsed "
@ -917,6 +985,10 @@ final class HttpClientImpl extends HttpClient implements Trackable {
HttpConnectTimeoutException hcte = new HttpConnectTimeoutException(msg);
hcte.initCause(throwable);
throw hcte;
} else if (throwable instanceof UnsupportedProtocolVersionException) {
var upve = new UnsupportedProtocolVersionException(msg);
upve.initCause(throwable);
throw upve;
} else if (throwable instanceof HttpTimeoutException) {
throw new HttpTimeoutException(msg);
} else if (throwable instanceof ConnectException) {
@ -972,6 +1044,13 @@ final class HttpClientImpl extends HttpClient implements Trackable {
return MinimalFuture.failedFuture(new IOException("closed"));
}
final HttpClient.Version vers = userRequest.version().orElse(this.version());
if (vers == Version.HTTP_3 && client3 == null
&& userRequest.getOption(H3_DISCOVERY).orElse(null) == HTTP_3_URI_ONLY) {
// HTTP3 isn't supported by this client
return MinimalFuture.failedFuture(new UnsupportedProtocolVersionException(
"HTTP3 is not supported"));
}
// should not happen, unless the selector manager has
// exited abnormally
if (selmgr.isClosed()) {
@ -1095,8 +1174,11 @@ final class HttpClientImpl extends HttpClient implements Trackable {
}
IOException selectorClosedException() {
var io = new IOException("selector manager closed");
var cause = errorRef.get();
final var cause = errorRef.get();
final String msg = cause == null
? "selector manager closed"
: "selector manager closed due to: " + cause;
final var io = new IOException(msg);
if (cause != null) {
io.initCause(cause);
}
@ -1181,6 +1263,10 @@ final class HttpClientImpl extends HttpClient implements Trackable {
}
// double check after closing
abortPendingRequests(owner, t);
var client3 = owner.client3;
if (client3 != null) {
client3.abort(t);
}
IOException io = toAbort.isEmpty()
? null : selectorClosedException();
@ -1456,8 +1542,8 @@ final class HttpClientImpl extends HttpClient implements Trackable {
String keyInterestOps = key.isValid()
? "key.interestOps=" + Utils.interestOps(key) : "invalid key";
return String.format("channel registered with selector, %s, sa.interestOps=%s",
keyInterestOps,
Utils.describeOps(((SelectorAttachment)key.attachment()).interestOps));
keyInterestOps,
Utils.describeOps(((SelectorAttachment)key.attachment()).interestOps));
} catch (Throwable t) {
return String.valueOf(t);
}
@ -1627,8 +1713,12 @@ final class HttpClientImpl extends HttpClient implements Trackable {
return Optional.ofNullable(connectTimeout);
}
Optional<Duration> idleConnectionTimeout() {
return Optional.ofNullable(getIdleConnectionTimeout());
Optional<Duration> idleConnectionTimeout(Version version) {
return switch (version) {
case HTTP_2 -> timeoutDuration(IDLE_CONNECTION_TIMEOUT_H2);
case HTTP_3 -> timeoutDuration(IDLE_CONNECTION_TIMEOUT_H3);
case HTTP_1_1 -> timeoutDuration(KEEP_ALIVE_TIMEOUT);
};
}
@Override
@ -1755,7 +1845,7 @@ final class HttpClientImpl extends HttpClient implements Trackable {
// error from here - but in this case there's not much we
// could do anyway. Just let it flow...
if (failed == null) failed = e;
else failed.addSuppressed(e);
else Utils.addSuppressed(failed, e);
Log.logTrace("Failed to handle event {0}: {1}", event, e);
}
}
@ -1799,10 +1889,11 @@ final class HttpClientImpl extends HttpClient implements Trackable {
return sslBufferSupplier;
}
private Duration getIdleConnectionTimeout() {
if (IDLE_CONNECTION_TIMEOUT >= 0)
return Duration.ofSeconds(IDLE_CONNECTION_TIMEOUT);
return null;
private Optional<Duration> timeoutDuration(long seconds) {
if (seconds >= 0) {
return Optional.of(Duration.ofSeconds(seconds));
}
return Optional.empty();
}
private static long getTimeoutProp(String prop, long def) {

View File

@ -30,6 +30,7 @@ import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.http.HttpResponse;
import java.nio.ByteBuffer;
import java.nio.channels.NetworkChannel;
import java.nio.channels.SocketChannel;
import java.util.Arrays;
import java.util.Comparator;
@ -57,7 +58,10 @@ import jdk.internal.net.http.common.SequentialScheduler;
import jdk.internal.net.http.common.SequentialScheduler.DeferredCompleter;
import jdk.internal.net.http.common.Log;
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;
import static jdk.internal.net.http.common.Utils.ProxyHeaders;
/**
@ -69,12 +73,13 @@ import static jdk.internal.net.http.common.Utils.ProxyHeaders;
* PlainTunnelingConnection: opens plain text (CONNECT) tunnel to server
* AsyncSSLConnection: TLS channel direct to server
* AsyncSSLTunnelConnection: TLS channel via (CONNECT) proxy tunnel
* HttpQuicConnection: direct QUIC connection to server
*/
abstract class HttpConnection implements Closeable {
final Logger debug = Utils.getDebugLogger(this::dbgString, Utils.DEBUG);
static final Logger DEBUG_LOGGER = Utils.getDebugLogger(
() -> "HttpConnection(SocketTube(?))", Utils.DEBUG);
() -> "HttpConnection", Utils.DEBUG);
public static final Comparator<HttpConnection> COMPARE_BY_ID
= Comparator.comparing(HttpConnection::id);
@ -112,8 +117,8 @@ abstract class HttpConnection implements Closeable {
this.label = label;
}
private static String nextLabel() {
return "" + LABEL_COUNTER.incrementAndGet();
private static String nextLabel(String prefix) {
return prefix + LABEL_COUNTER.incrementAndGet();
}
/**
@ -198,9 +203,17 @@ abstract class HttpConnection implements Closeable {
abstract InetSocketAddress proxy();
/** Tells whether, or not, this connection is open. */
final boolean isOpen() {
boolean isOpen() {
return channel().isOpen() &&
(connected() ? !getConnectionFlow().isFinished() : true);
(connected() ? !isFlowFinished() : true);
}
/**
* {@return {@code true} if the {@linkplain #getConnectionFlow()
* connection flow} is {@linkplain FlowTube#isFinished() finished}.
*/
boolean isFlowFinished() {
return getConnectionFlow().isFinished();
}
/**
@ -232,13 +245,17 @@ abstract class HttpConnection implements Closeable {
* still open, and the method returns true.
* @return true if the channel appears to be still open.
*/
final boolean checkOpen() {
boolean checkOpen() {
if (isOpen()) {
try {
// channel is non blocking
int read = channel().read(ByteBuffer.allocate(1));
if (read == 0) return true;
close();
if (channel() instanceof SocketChannel channel) {
int read = channel.read(ByteBuffer.allocate(1));
if (read == 0) return true;
close();
} else {
return channel().isOpen();
}
} catch (IOException x) {
debug.log("Pooled connection is no longer operational: %s",
x.toString());
@ -294,6 +311,7 @@ abstract class HttpConnection implements Closeable {
* is one of the following:
* {@link PlainHttpConnection}
* {@link PlainTunnelingConnection}
* {@link HttpQuicConnection}
*
* The returned connection, if not from the connection pool, must have its,
* connect() or connectAsync() method invoked, which ( when it completes
@ -301,6 +319,7 @@ abstract class HttpConnection implements Closeable {
*/
public static HttpConnection getConnection(InetSocketAddress addr,
HttpClientImpl client,
Exchange<?> exchange,
HttpRequestImpl request,
Version version) {
// The default proxy selector may select a proxy whose address is
@ -322,18 +341,27 @@ abstract class HttpConnection implements Closeable {
return getPlainConnection(addr, proxy, request, client);
}
} else { // secure
if (version != HTTP_2) { // only HTTP/1.1 connections are in the pool
if (version == HTTP_1_1) { // only HTTP/1.1 connections are in the pool
c = pool.getConnection(true, addr, proxy);
}
if (c != null && c.isOpen()) {
final HttpConnection conn = c;
if (DEBUG_LOGGER.on())
DEBUG_LOGGER.log(conn.getConnectionFlow()
+ ": SSL connection retrieved from HTTP/1.1 pool");
if (DEBUG_LOGGER.on()) {
DEBUG_LOGGER.log(c.getConnectionFlow()
+ ": SSL connection retrieved from HTTP/1.1 pool");
}
return c;
} else if (version == HTTP_3 && client.client3().isPresent()) {
// We only come here after we have checked the HTTP/3 connection pool,
// and if the client config supports HTTP/3
if (DEBUG_LOGGER.on())
DEBUG_LOGGER.log("Attempting to get an HTTP/3 connection");
return HttpQuicConnection.getHttpQuicConnection(addr, proxy, request, exchange, client);
} else {
assert !request.isHttp3Only(version); // should have failed before
String[] alpn = null;
if (version == HTTP_2 && hasRequiredHTTP2TLSVersion(client)) {
// We only come here after we have checked the HTTP/2 connection pool.
// We will not negotiate HTTP/2 if we don't have the appropriate TLS version
alpn = new String[] { Alpns.H2, Alpns.HTTP_1_1 };
}
return getSSLConnection(addr, proxy, alpn, request, client);
@ -346,7 +374,7 @@ abstract class HttpConnection implements Closeable {
String[] alpn,
HttpRequestImpl request,
HttpClientImpl client) {
final String label = nextLabel();
final String label = nextLabel("tls:");
final Origin originServer;
try {
originServer = Origin.from(request.uri());
@ -433,7 +461,7 @@ abstract class HttpConnection implements Closeable {
InetSocketAddress proxy,
HttpRequestImpl request,
HttpClientImpl client) {
final String label = nextLabel();
final String label = nextLabel("tcp:");
final Origin originServer;
try {
originServer = Origin.from(request.uri());
@ -483,7 +511,7 @@ abstract class HttpConnection implements Closeable {
/* Tells whether or not this connection is a tunnel through a proxy */
boolean isTunnel() { return false; }
abstract SocketChannel channel();
abstract NetworkChannel channel();
final InetSocketAddress address() {
return address;
@ -516,6 +544,19 @@ abstract class HttpConnection implements Closeable {
close();
}
/**
* {@return the underlying connection flow, if applicable}
*
* @apiNote
* TCP based protocols like HTTP/1.1 and HTTP/2 are built on
* top of a {@linkplain FlowTube bidirectional connection flow}.
* On the other hand, Quic based protocol like HTTP/3 are
* multiplexed at the Quic level, and therefore do not have
* a connection flow.
*
* @throws IllegalStateException if the underlying transport
* does not expose a single connection flow.
*/
abstract FlowTube getConnectionFlow();
/**

View File

@ -0,0 +1,690 @@
/*
* 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.ConnectException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.SocketOption;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpConnectTimeoutException;
import java.nio.channels.NetworkChannel;
import java.time.Duration;
import java.util.Arrays;
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.TimeUnit;
import java.util.function.Function;
import java.util.function.Predicate;
import javax.net.ssl.SNIServerName;
import javax.net.ssl.SSLParameters;
import jdk.internal.net.http.ConnectionPool.CacheKey;
import jdk.internal.net.http.AltServicesRegistry.AltService;
import jdk.internal.net.http.common.FlowTube;
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.quic.ConnectionTerminator;
import jdk.internal.net.http.quic.TerminationCause;
import jdk.internal.net.http.quic.QuicConnection;
import static jdk.internal.net.http.Http3ClientProperties.MAX_DIRECT_CONNECTION_TIMEOUT;
import static jdk.internal.net.http.common.Alpns.H3;
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.quic.TerminationCause.appLayerClose;
import static jdk.internal.net.http.quic.TerminationCause.appLayerException;
/**
* An {@code HttpQuicConnection} models an HTTP connection over
* QUIC.
* The particulars of the HTTP/3 protocol are handled by the
* Http3Connection class.
*/
abstract class HttpQuicConnection extends HttpConnection {
final Logger debug = Utils.getDebugLogger(this::quicDbgString);
final QuicConnection quicConnection;
final ConnectionTerminator quicConnTerminator;
// the alt-service which was advertised, from some origin, for this connection co-ordinates.
// can be null, which indicates this wasn't created because of an alt-service
private final AltService sourceAltService;
// HTTP/2 MUST use TLS version 1.3 or higher for HTTP/3 over TLS
private static final Predicate<String> testRequiredHTTP3TLSVersion = proto ->
proto.equals("TLSv1.3");
HttpQuicConnection(Origin originServer, InetSocketAddress address, HttpClientImpl client,
QuicConnection quicConnection, AltService sourceAltService) {
super(originServer, address, client, "quic:" + quicConnection.uniqueId());
Objects.requireNonNull(quicConnection);
this.quicConnection = quicConnection;
this.quicConnTerminator = quicConnection.connectionTerminator();
this.sourceAltService = sourceAltService;
}
/**
* A HTTP QUIC connection could be created due to an alt-service that was advertised
* from some origin. This method returns that source alt-service if there was one.
* @return The source alt-service if present
*/
Optional<AltService> getSourceAltService() {
return Optional.ofNullable(this.sourceAltService);
}
@Override
public List<SNIServerName> getSNIServerNames() {
final SSLParameters sslParams = this.quicConnection.getTLSEngine().getSSLParameters();
if (sslParams == null) {
return List.of();
}
final List<SNIServerName> sniServerNames = sslParams.getServerNames();
if (sniServerNames == null) {
return List.of();
}
return List.copyOf(sniServerNames);
}
final String quicDbgString() {
String tag = dbgTag;
if (tag == null) tag = dbgTag = "Http" + quicConnection.dbgTag();
return tag;
}
/**
* Initiates the connect phase.
*
* Returns a CompletableFuture that completes when the underlying
* TCP connection has been established or an error occurs.
*/
public abstract CompletableFuture<Void> connectAsync(Exchange<?> exchange);
private volatile boolean connected;
/**
* Finishes the connection phase.
*
* Returns a CompletableFuture that completes when any additional,
* type specific, setup has been done. Must be called after connectAsync.
*/
public CompletableFuture<Void> finishConnect() {
this.connected = true;
return MinimalFuture.completedFuture(null);
}
/** Tells whether, or not, this connection is connected to its destination. */
boolean connected() {
return connected;
}
/** Tells whether, or not, this connection is secure ( over SSL ) */
final boolean isSecure() { return true; } // QUIC is secure
/**
* Tells whether, or not, this connection is proxied.
* Returns true for tunnel connections, or clear connection to
* any host through proxy.
*/
final boolean isProxied() { return false;} // Proxy not supported
/**
* Returns the address of the proxy used by this connection.
* Returns the proxy address for tunnel connections, or
* clear connection to any host through proxy.
* Returns {@code null} otherwise.
*/
final InetSocketAddress proxy() { return null; } // Proxy not supported
/**
* This method throws an {@link UnsupportedOperationException}
*/
@Override
final HttpPublisher publisher() {
throw new UnsupportedOperationException("no publisher for a quic connection");
}
QuicConnection quicConnection() {
return quicConnection;
}
/**
* Returns true if the given client's SSL parameter protocols contains at
* least one TLS version that HTTP/3 requires.
*/
private static boolean hasRequiredHTTP3TLSVersion(HttpClient client) {
String[] protos = client.sslParameters().getProtocols();
if (protos != null) {
return Arrays.stream(protos).anyMatch(testRequiredHTTP3TLSVersion);
} else {
return false;
}
}
/**
* Called when the HTTP/3 connection is established, either successfully or
* unsuccessfully
* @param connection the HTTP/3 connection, if successful, or null, otherwise
* @param throwable the exception encountered, if unsuccessful
*/
public abstract void connectionEstablished(Http3Connection connection,
Throwable throwable);
/**
* A functional interface used to update the Alternate Service Registry
* after a direct connection attempt.
*/
@FunctionalInterface
private interface DirectConnectionUpdater {
/**
* This method may update the HttpClient registry, or
* {@linkplain Http3ClientImpl#noH3(String) record the unsuccessful}
* direct connection attempt.
*
* @param conn the connection or null
* @param throwable the exception or null
*/
void onConnectionEstablished(
Http3Connection conn, Throwable throwable);
/**
* Does nothing
* @param conn the connection
* @param throwable the exception
*/
static void noUpdate(
Http3Connection conn, Throwable throwable) {
}
}
/**
* This method create and return a new unconnected HttpQuicConnection,
* wrapping a {@link QuicConnection}. May return {@code null} if
* HTTP/3 is not supported with the given parameters. For instance,
* if TLSv1.3 isn't available/enabled in the client's SSLParameters,
* or if ALT_SERVICE is required but no alt service is found.
*
* @param addr the HTTP/3 peer endpoint address, if direct connection
* @param proxy the proxy address, if a proxy is used, in which case this
* method will return {@code null} as proxying is not supported
* with HTTP/3
* @param request the request for which the connection is being created
* @param exchange the exchange for which the connection is being created
* @param client the HttpClientImpl instance
* @return A new HttpQuicConnection or {@code null}
*/
public static HttpQuicConnection getHttpQuicConnection(final InetSocketAddress addr,
final InetSocketAddress proxy,
final HttpRequestImpl request,
final Exchange<?> exchange,
final HttpClientImpl client) {
if (!client.client3().isPresent()) {
if (Log.http3()) {
Log.logHttp3("HTTP3 isn't supported by the client");
}
return null;
}
final Http3ClientImpl h3client = client.client3().get();
// HTTP_3 with proxy not supported; In this case we will downgrade
// to using HTTP/2
var debug = h3client.debug();
var where = "HttpQuicConnection.getHttpQuicConnection";
if (proxy != null || !hasRequiredHTTP3TLSVersion(client)) {
if (debug.on())
debug.log("%s: proxy required or SSL version mismatch", where);
return null;
}
assert request.secure();
// Question: Do we need this scaffolding?
// I mean - could Http3Connection and HttpQuicConnection be the same
// object?
// Answer: Http3Connection models an established connection which is
// ready to be used.
// HttpQuicConnection serves at establishing a new Http3Connection
// => Http3Connection is pooled, HttpQuicConnection is not.
// => Do we need HttpQuicConnection vs QuicConnection?
// => yes: HttpQuicConnection can access all package protected
// APIs in HttpConnection & al
// QuicConnection is in the quic subpackage.
// HttpQuicConnection makes the necessary adaptation between
// HttpConnection and QuicConnection.
// find whether we have an alternate service access point for HTTP/3
// if we do, create a new QuicConnection and a new Http3Connection over it.
var uri = request.uri();
var config = request.http3Discovery();
if (debug.on()) {
debug.log("Checking ALT-SVC regardless of H3_DISCOVERY settings");
}
// we only support H3 right now
var altSvc = client.registry()
.lookup(uri, H3::equals)
.findFirst().orElse(null);
Optional<Duration> directTimeout = Optional.empty();
final boolean advertisedAltSvc = altSvc != null && altSvc.wasAdvertised();
logAltSvcFor(debug, uri, altSvc, where);
switch (config) {
case ALT_SVC: {
if (!advertisedAltSvc) {
// fallback to HTTP/2
if (altSvc != null) {
if (Log.altsvc()) {
Log.logAltSvc("{0}: Cannot use unadvertised AltService: {1}",
config, altSvc);
}
}
return null;
}
assert altSvc != null && altSvc.wasAdvertised();
break;
}
// attempt direct connection if HTTP/3 only
case HTTP_3_URI_ONLY: {
if (advertisedAltSvc && !altSvc.originHasSameAuthority()) {
if (Log.altsvc()) {
Log.logAltSvc("{0}: Cannot use advertised AltService: {1}",
config, altSvc);
}
altSvc = null;
}
assert altSvc == null || altSvc.originHasSameAuthority();
break;
}
default: {
// if direct connection already attempted and failed,
// fallback to HTTP/2
if (altSvc == null && h3client.hasNoH3(uri.getRawAuthority())) {
return null;
}
if (!advertisedAltSvc) {
// directTimeout is only used for happy eyeball
Duration def = Duration.ofMillis(MAX_DIRECT_CONNECTION_TIMEOUT);
Duration timeout = client.connectTimeout()
.filter(d -> d.compareTo(def) <= 0)
.orElse(def);
directTimeout = Optional.of(timeout);
}
break;
}
}
if (altSvc != null) {
assert H3.equals(altSvc.alpn());
Log.logAltSvc("{0}: Using AltService for {1}: {2}",
config, uri.getRawAuthority(), altSvc);
}
if (debug.on()) {
debug.log("%s: creating QuicConnection for: %s", where, uri);
}
final QuicConnection quicConnection = (altSvc != null) ?
h3client.quicClient().createConnectionFor(altSvc) :
h3client.quicClient().createConnectionFor(addr, new String[] {H3});
if (debug.on()) debug.log("%s: QuicConnection: %s", where, quicConnection);
final DirectConnectionUpdater onConnectFinished = advertisedAltSvc
? DirectConnectionUpdater::noUpdate
: (c,t) -> registerUnadvertised(client, uri, addr, c, t);
// Note: we could get rid of the updater by introducing
// H3DirectQuicConnectionImpl extends H3QuicConnectionImpl
HttpQuicConnection httpQuicConn = new H3QuicConnectionImpl(Origin.from(request.uri()), addr, client,
quicConnection, onConnectFinished, directTimeout, altSvc);
// if we created a connection and if that connection is to an (advertised) alt service then
// we setup the Exchange's request to include the "alt-used" header to refer to the
// alt service that was used (section 5, RFC-7838)
if (httpQuicConn != null && altSvc != null && advertisedAltSvc) {
exchange.request().setSystemHeader("alt-used", altSvc.authority());
}
return httpQuicConn;
}
private static void logAltSvcFor(Logger debug, URI uri, AltService altSvc, String where) {
if (altSvc == null) {
if (Log.altsvc()) {
Log.logAltSvc("No AltService found for {0}", uri.getRawAuthority());
} else if (debug.on()) {
debug.log("%s: No ALT-SVC for %s", where, uri.getRawAuthority());
}
} else {
if (debug.on()) debug.log("%s: ALT-SVC: %s", where, altSvc);
}
}
static void registerUnadvertised(final HttpClientImpl client,
final URI requestURI,
final InetSocketAddress destAddr,
final Http3Connection connection,
final Throwable t) {
if (t == null && connection != null) {
// There is an h3 endpoint at the given origin: update the registry
final Origin origin = connection.connection().getOriginServer();
assert origin != null : "origin server is null on connection: "
+ connection.connection();
assert origin.port() == destAddr.getPort();
var id = new AltService.Identity(H3, origin.host(), origin.port());
client.registry().registerUnadvertised(id, origin, connection.connection());
return;
}
if (t != null) {
assert client.client3().isPresent() : "HTTP3 isn't supported by the client";
final URI originURI = requestURI.resolve("/");
// record that there is no h3 at the given origin
client.client3().get().noH3(originURI.getRawAuthority());
}
}
// TODO: we could probably merge H3QuicConnectionImpl with HttpQuicConnection now
static class H3QuicConnectionImpl extends HttpQuicConnection {
private final Optional<Duration> directTimeout;
private final DirectConnectionUpdater connFinishedAction;
H3QuicConnectionImpl(Origin originServer,
InetSocketAddress address,
HttpClientImpl client,
QuicConnection quic,
DirectConnectionUpdater connFinishedAction,
Optional<Duration> directTimeout,
AltService sourceAltService) {
super(originServer, address, client, quic, sourceAltService);
this.directTimeout = directTimeout;
this.connFinishedAction = connFinishedAction;
}
@Override
public CompletableFuture<Void> connectAsync(Exchange<?> exchange) {
var request = exchange.request();
var uri = request.uri();
// Adapt HandshakeCF to CompletableFuture<Void>
CompletableFuture<CompletableFuture<Void>> handshakeCfCf =
quicConnection.startHandshake()
.handle((r, t) -> {
if (t == null) {
// successful handshake
return MinimalFuture.completedFuture(r);
}
final TerminationCause terminationCause = quicConnection.terminationCause();
final boolean appLayerTermination = terminationCause != null
&& terminationCause.isAppLayer();
// QUIC connection handshake failed. we now decide whether we should
// unregister the alt-service (if any) that was the source of this
// connection attempt.
//
// handshake could have failed for one of several reasons, some of them:
// - something at QUIC layer caused the failure (either some internal
// exception or protocol error or QUIC TLS error)
// - or the app layer, through the HttpClient/HttpConnection
// could have triggered a connection close.
//
// we unregister the alt-service (if any) only if the termination cause
// originated in the QUIC layer. An app layer termination cause doesn't
// necessarily mean that the alt-service isn't valid for subsequent use.
if (!appLayerTermination && this.getSourceAltService().isPresent()) {
final AltService altSvc = this.getSourceAltService().get();
if (debug.on()) {
debug.log("connection attempt to an alternate service at "
+ altSvc.authority() + " failed during handshake: " + t);
}
client().registry().markInvalid(this.getSourceAltService().get());
// fail with ConnectException to allow the request to potentially
// be retried on a different connection
final ConnectException connectException = new ConnectException(
"QUIC connection handshake to an alternate service failed");
connectException.initCause(t);
return MinimalFuture.failedFuture(connectException);
} else {
// alt service wasn't the cause of this failed connection attempt.
// return a failed future with the original cause
return MinimalFuture.failedFuture(t);
}
})
.thenApply((handshakeCompletion) -> {
if (handshakeCompletion.isCompletedExceptionally()) {
return MinimalFuture.failedFuture(handshakeCompletion.exceptionNow());
}
return MinimalFuture.completedFuture(null);
});
// In case of direct connection, set up a timeout on the handshakeReachedPeerCf,
// and arrange for it to complete the handshakeCfCf above with a timeout in
// case that timeout expires...
if (directTimeout.isPresent()) {
debug.log("setting up quic direct connect timeout: " + directTimeout.get().toMillis());
var handshakeReachedPeerCf = quicConnection.handshakeReachedPeer();
CompletableFuture<CompletableFuture<Void>> fxi2 = handshakeReachedPeerCf
.thenApply((unused) -> MinimalFuture.completedFuture(null));
fxi2 = fxi2.completeOnTimeout(
MinimalFuture.failedFuture(new HttpConnectTimeoutException("quic handshake timeout")),
directTimeout.get().toMillis(), TimeUnit.MILLISECONDS);
fxi2.handleAsync((r, t) -> {
if (t != null) {
var cause = Utils.getCompletionCause(t);
// arrange for handshakeCfCf to timeout
handshakeCfCf.completeExceptionally(cause);
}
if (r.isCompletedExceptionally()) {
var cause = Utils.getCompletionCause(r.exceptionNow());
// arrange for handshakeCfCf to timeout
handshakeCfCf.completeExceptionally(cause);
}
return r;
}, exchange.parentExecutor.safeDelegate());
}
Optional<Duration> timeout = client().connectTimeout();
CompletableFuture<CompletableFuture<Void>> fxi = handshakeCfCf;
// In case of connection timeout, set up a timeout on the handshakeCfCf.
// Note: this is a different timeout than the direct connection timeout.
if (timeout.isPresent()) {
// In case of timeout we need to close the quic connection
debug.log("setting up quic connect timeout: " + timeout.get().toMillis());
fxi = handshakeCfCf.completeOnTimeout(
MinimalFuture.failedFuture(new HttpConnectTimeoutException("quic connect timeout")),
timeout.get().toMillis(), TimeUnit.MILLISECONDS);
}
// If we have set up any timeout, arrange to close the quicConnection
// if one of the timeout expires
if (timeout.isPresent() || directTimeout.isPresent()) {
fxi = fxi.handleAsync(this::handleTimeout, exchange.parentExecutor.safeDelegate());
}
return fxi.thenCompose(Function.identity());
}
@Override
public void connectionEstablished(Http3Connection connection,
Throwable throwable) {
connFinishedAction.onConnectionEstablished(connection, throwable);
}
private <U> CompletableFuture<U> handleTimeout(CompletableFuture<U> r, Throwable t) {
if (t != null) {
if (Utils.getCompletionCause(t) instanceof HttpConnectTimeoutException te) {
debug.log("Timeout expired: " + te);
close(H3_NO_ERROR.code(), "timeout expired", te);
return MinimalFuture.failedFuture(te);
}
return MinimalFuture.failedFuture(t);
} else if (r.isCompletedExceptionally()) {
t = r.exceptionNow();
if (Utils.getCompletionCause(t) instanceof HttpConnectTimeoutException te) {
debug.log("Completed in timeout: " + te);
close(H3_NO_ERROR.code(), "timeout expired", te);
}
}
return r;
}
@Override
NetworkChannel /* DatagramChannel */ channel() {
// Note: revisit this
// - don't return a new instance each time
// - see if we could avoid exposing
// the channel in the first place
H3QuicConnectionImpl self = this;
return new NetworkChannel() {
@Override
public NetworkChannel bind(SocketAddress local) throws IOException {
throw new UnsupportedOperationException("no bind for a quic connection");
}
@Override
public SocketAddress getLocalAddress() throws IOException {
return quicConnection.localAddress();
}
@Override
public <T> NetworkChannel setOption(SocketOption<T> name, T value) throws IOException {
return this;
}
@Override
public <T> T getOption(SocketOption<T> name) throws IOException {
return null;
}
@Override
public Set<SocketOption<?>> supportedOptions() {
return Set.of();
}
@Override
public boolean isOpen() {
return quicConnection.isOpen();
}
@Override
public void close() throws IOException {
self.close();
}
};
}
@Override
CacheKey cacheKey() {
return null;
}
// close with H3_NO_ERROR
@Override
public final void close() {
close(H3_NO_ERROR.code(), "connection closed", null);
}
@Override
void close(final Throwable cause) {
close(H3_INTERNAL_ERROR.code(), null, cause);
}
}
/* Tells whether this connection is a tunnel through a proxy */
boolean isTunnel() { return false; }
abstract NetworkChannel /* DatagramChannel */ channel();
abstract ConnectionPool.CacheKey cacheKey();
/**
* Closes the underlying transport connection with
* the given {@code connCloseCode} code. This will be considered a application
* layer close and will generate a {@code ConnectionCloseFrame}
* of type {@code 0x1d} as the cause of the termination.
*
* @param connCloseCode the connection close code
* @param logMsg the message to be included in the logs as
* the cause of the connection termination. can be null.
* @param closeCause the underlying cause of the connection termination. can be null,
* in which case just the {@code error} will be recorded as the
* cause of the connection termination.
*/
final void close(final long connCloseCode, final String logMsg,
final Throwable closeCause) {
final TerminationCause terminationCause;
if (closeCause == null) {
terminationCause = appLayerClose(connCloseCode);
} else {
terminationCause = appLayerException(connCloseCode, closeCause);
}
// set the log message only if non-null, else let it default to internal
// implementation sensible default
if (logMsg != null) {
terminationCause.loggedAs(logMsg);
}
quicConnTerminator.terminate(terminationCause);
}
abstract void close(final Throwable t);
/**
* {@inheritDoc}
*
* @implSpec
* Unlike HTTP/1.1 and HTTP/2, an HTTP/3 connection is not
* built on a single connection flow, since multiplexing is
* provided by the lower layer. Therefore, the higher HTTP
* layer should never call {@code getConnectionFlow()} on an
* {@link HttpQuicConnection}. As a consequence, this method
* always throws {@link IllegalStateException} unconditionally.
*
* @return nothing: this method always throw {@link IllegalStateException}
*
* @throws IllegalStateException always
*/
@Override
final FlowTube getConnectionFlow() {
throw new IllegalStateException(
"An HTTP/3 connection does not expose " +
"a single connection flow");
}
/**
* Unlike HTTP/1.1 and HTTP/2, an HTTP/3 connection is not
* built on a single connection flow, since multiplexing is
* provided by the lower layer. This method instead will
* return {@code true} if the underlying quic connection
* has been terminated, either exceptionally or normally.
*
* @return {@code true} if the underlying Quic connection
* has been terminated.
*/
@Override
boolean isFlowFinished() {
return !quicConnection().isOpen();
}
@Override
public String toString() {
return quicDbgString();
}
}

View File

@ -26,12 +26,18 @@
package jdk.internal.net.http;
import java.net.URI;
import java.net.http.HttpRequest.Builder;
import java.net.http.HttpOption;
import java.time.Duration;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublisher;
import java.util.Set;
import jdk.internal.net.http.common.HttpHeadersBuilder;
import jdk.internal.net.http.common.Utils;
@ -51,6 +57,10 @@ public class HttpRequestBuilderImpl implements HttpRequest.Builder {
private BodyPublisher bodyPublisher;
private volatile Optional<HttpClient.Version> version;
private Duration duration;
private final Map<HttpOption<?>, Object> options = new HashMap<>();
private static final Set<HttpOption<?>> supportedOptions =
Set.of(HttpOption.H3_DISCOVERY);
public HttpRequestBuilderImpl(URI uri) {
requireNonNull(uri, "uri must be non-null");
@ -100,6 +110,7 @@ public class HttpRequestBuilderImpl implements HttpRequest.Builder {
b.uri = uri;
b.duration = duration;
b.version = version;
b.options.putAll(Map.copyOf(options));
return b;
}
@ -158,6 +169,19 @@ public class HttpRequestBuilderImpl implements HttpRequest.Builder {
return this;
}
@Override
public <T> Builder setOption(HttpOption<T> option, T value) {
Objects.requireNonNull(option, "option");
if (value == null) options.remove(option);
else if (supportedOptions.contains(option)) {
if (!option.type().isInstance(value)) {
throw newIAE("Illegal value type %s for %s", value, option);
}
options.put(option, value);
} // otherwise just ignore the option
return this;
}
HttpHeadersBuilder headersBuilder() { return headersBuilder; }
URI uri() { return uri; }
@ -170,6 +194,8 @@ public class HttpRequestBuilderImpl implements HttpRequest.Builder {
Optional<HttpClient.Version> version() { return version; }
Map<HttpOption<?>, Object> options() { return options; }
@Override
public HttpRequest.Builder GET() {
return method0("GET", null);
@ -245,4 +271,30 @@ public class HttpRequestBuilderImpl implements HttpRequest.Builder {
Duration timeout() { return duration; }
public static Map<HttpOption<?>, Object> copySupportedOptions(HttpRequest request) {
Objects.requireNonNull(request, "request");
if (request instanceof ImmutableHttpRequest ihr) {
// already checked and immutable
return ihr.options();
}
Map<HttpOption<?>, Object> options = new HashMap<>();
for (HttpOption<?> option : supportedOptions) {
var val = request.getOption(option);
if (!val.isPresent()) continue;
options.put(option, option.type().cast(val.get()));
}
return Map.copyOf(options);
}
public static Map<HttpOption<?>, Object> copySupportedOptions(Map<HttpOption<?>, Object> options) {
Objects.requireNonNull(options, "option");
Map<HttpOption<?>, Object> result = new HashMap<>();
for (HttpOption<?> option : supportedOptions) {
var val = options.get(option);
if (val == null) continue;
result.put(option, option.type().cast(val));
}
return Map.copyOf(result);
}
}

View File

@ -31,9 +31,13 @@ import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.URI;
import java.net.http.HttpClient.Version;
import java.net.http.HttpOption;
import java.net.http.HttpOption.Http3DiscoveryMode;
import java.time.Duration;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.net.http.HttpClient;
@ -46,6 +50,7 @@ import jdk.internal.net.http.common.HttpHeadersBuilder;
import jdk.internal.net.http.common.Utils;
import jdk.internal.net.http.websocket.WebSocketRequest;
import static java.net.http.HttpOption.H3_DISCOVERY;
import static java.net.Authenticator.RequestorType.SERVER;
import static jdk.internal.net.http.common.Utils.ALLOWED_HEADERS;
import static jdk.internal.net.http.common.Utils.ProxyHeaders;
@ -65,6 +70,8 @@ public class HttpRequestImpl extends HttpRequest implements WebSocketRequest {
private volatile boolean isWebSocket;
private final Duration timeout; // may be null
private final Optional<HttpClient.Version> version;
// An alternative would be to have one field per supported option
private final Map<HttpOption<?>, Object> options;
private volatile boolean userSetAuthorization;
private volatile boolean userSetProxyAuthorization;
@ -92,6 +99,7 @@ public class HttpRequestImpl extends HttpRequest implements WebSocketRequest {
this.requestPublisher = builder.bodyPublisher(); // may be null
this.timeout = builder.timeout();
this.version = builder.version();
this.options = Map.copyOf(builder.options());
this.authority = null;
}
@ -110,12 +118,13 @@ public class HttpRequestImpl extends HttpRequest implements WebSocketRequest {
"uri must be non null");
Duration timeout = request.timeout().orElse(null);
this.method = method == null ? "GET" : method;
this.options = HttpRequestBuilderImpl.copySupportedOptions(request);
this.userHeaders = HttpHeaders.of(request.headers().map(), Utils.VALIDATE_USER_HEADER);
if (request instanceof HttpRequestImpl) {
if (request instanceof HttpRequestImpl impl) {
// all cases exception WebSocket should have a new system headers
this.isWebSocket = ((HttpRequestImpl) request).isWebSocket;
this.isWebSocket = impl.isWebSocket;
if (isWebSocket) {
this.systemHeadersBuilder = ((HttpRequestImpl)request).systemHeadersBuilder;
this.systemHeadersBuilder = impl.systemHeadersBuilder;
} else {
this.systemHeadersBuilder = new HttpHeadersBuilder();
}
@ -199,6 +208,19 @@ public class HttpRequestImpl extends HttpRequest implements WebSocketRequest {
this.timeout = other.timeout;
this.version = other.version();
this.authority = null;
this.options = other.optionsFor(this.uri);
}
private Map<HttpOption<?>, Object> optionsFor(URI uri) {
if (this.uri == uri || Objects.equals(this.uri.getRawAuthority(), uri.getRawAuthority())) {
return options;
}
// preserve config if version is HTTP/3
if (version.orElse(null) == Version.HTTP_3) {
Http3DiscoveryMode h3DiscoveryMode = (Http3DiscoveryMode)options.get(H3_DISCOVERY);
if (h3DiscoveryMode != null) return Map.of(H3_DISCOVERY, h3DiscoveryMode);
}
return Map.of();
}
private BodyPublisher publisher(HttpRequestImpl other) {
@ -234,12 +256,26 @@ public class HttpRequestImpl extends HttpRequest implements WebSocketRequest {
// What we want to possibly upgrade is the tunneled connection to the
// target server (so not the CONNECT request itself)
this.version = Optional.of(HttpClient.Version.HTTP_1_1);
this.options = Map.of();
}
final boolean isConnect() {
return "CONNECT".equalsIgnoreCase(method);
}
final boolean isHttp3Only(Version version) {
return version == Version.HTTP_3 && http3Discovery() == HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY;
}
final Http3DiscoveryMode http3Discovery() {
// see if discovery mode is set on the request
final var h3Discovery = getOption(H3_DISCOVERY);
// if no explicit discovery mode is set, then default to "ANY"
// irrespective of whether the HTTP/3 version may have been
// set on the HttpClient or the HttpRequest
return h3Discovery.orElse(Http3DiscoveryMode.ANY);
}
/**
* Creates a HttpRequestImpl from the given set of Headers and the associated
* "parent" request. Fields not taken from the headers are taken from the
@ -276,6 +312,7 @@ public class HttpRequestImpl extends HttpRequest implements WebSocketRequest {
this.timeout = parent.timeout;
this.version = parent.version;
this.authority = null;
this.options = parent.options;
}
@Override
@ -399,6 +436,11 @@ public class HttpRequestImpl extends HttpRequest implements WebSocketRequest {
@Override
public Optional<HttpClient.Version> version() { return version; }
@Override
public <T> Optional<T> getOption(HttpOption<T> option) {
return Optional.ofNullable(option.type().cast(options.get(option)));
}
@Override
public void setSystemHeader(String name, String value) {
systemHeadersBuilder.setHeader(name, value);

View File

@ -41,14 +41,14 @@ import jdk.internal.net.http.websocket.RawChannel;
/**
* The implementation class for HttpResponse
*/
class HttpResponseImpl<T> implements HttpResponse<T>, RawChannel.Provider {
final class HttpResponseImpl<T> implements HttpResponse<T>, RawChannel.Provider {
final int responseCode;
private final String connectionLabel;
final HttpRequest initialRequest;
final Optional<HttpResponse<T>> previousResponse;
final HttpResponse<T> previousResponse; // may be null;
final HttpHeaders headers;
final Optional<SSLSession> sslSession;
final SSLSession sslSession; // may be null
final URI uri;
final HttpClient.Version version;
final RawChannelProvider rawChannelProvider;
@ -62,10 +62,10 @@ class HttpResponseImpl<T> implements HttpResponse<T>, RawChannel.Provider {
this.responseCode = response.statusCode();
this.connectionLabel = connectionLabel(exch).orElse(null);
this.initialRequest = initialRequest;
this.previousResponse = Optional.ofNullable(previousResponse);
this.previousResponse = previousResponse;
this.headers = response.headers();
//this.trailers = trailers;
this.sslSession = Optional.ofNullable(response.getSSLSession());
this.sslSession = response.getSSLSession();
this.uri = response.request().uri();
this.version = response.version();
this.rawChannelProvider = RawChannelProvider.create(response, exch);
@ -96,7 +96,7 @@ class HttpResponseImpl<T> implements HttpResponse<T>, RawChannel.Provider {
@Override
public Optional<HttpResponse<T>> previousResponse() {
return previousResponse;
return Optional.ofNullable(previousResponse);
}
@Override
@ -111,7 +111,7 @@ class HttpResponseImpl<T> implements HttpResponse<T>, RawChannel.Provider {
@Override
public Optional<SSLSession> sslSession() {
return sslSession;
return Optional.ofNullable(sslSession);
}
@Override

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2018, 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
@ -28,7 +28,9 @@ package jdk.internal.net.http;
import java.net.URI;
import java.net.http.HttpHeaders;
import java.net.http.HttpRequest;
import java.net.http.HttpOption;
import java.time.Duration;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.net.http.HttpClient.Version;
@ -43,6 +45,8 @@ final class ImmutableHttpRequest extends HttpRequest {
private final boolean expectContinue;
private final Optional<Duration> timeout;
private final Optional<Version> version;
// An alternative would be to have one field per supported option
private final Map<HttpOption<?>, Object> options;
/** Creates an ImmutableHttpRequest from the given builder. */
ImmutableHttpRequest(HttpRequestBuilderImpl builder) {
@ -53,6 +57,7 @@ final class ImmutableHttpRequest extends HttpRequest {
this.expectContinue = builder.expectContinue();
this.timeout = Optional.ofNullable(builder.timeout());
this.version = Objects.requireNonNull(builder.version());
this.options = Map.copyOf(builder.options());
}
@Override
@ -78,8 +83,17 @@ final class ImmutableHttpRequest extends HttpRequest {
@Override
public Optional<Version> version() { return version; }
@Override
public <T> Optional<T> getOption(HttpOption<T> option) {
return Optional.ofNullable(option.type().cast(options.get(option)));
}
@Override
public String toString() {
return uri.toString() + " " + method;
}
public Map<HttpOption<?>, Object> options() {
return options;
}
}

View File

@ -29,7 +29,9 @@ import java.io.IOError;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.net.ConnectException;
import java.net.http.HttpClient.Version;
import java.net.http.HttpConnectTimeoutException;
import java.net.http.StreamLimitException;
import java.time.Duration;
import java.util.List;
import java.util.ListIterator;
@ -38,8 +40,6 @@ import java.util.Optional;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Flow;
import java.util.concurrent.atomic.AtomicInteger;
@ -62,6 +62,8 @@ import jdk.internal.net.http.common.ConnectionExpiredException;
import jdk.internal.net.http.common.Utils;
import static jdk.internal.net.http.common.MinimalFuture.completedFuture;
import static jdk.internal.net.http.common.MinimalFuture.failedFuture;
import static jdk.internal.net.http.AltSvcProcessor.processAltSvcHeader;
/**
* Encapsulates multiple Exchanges belonging to one HttpRequestImpl.
@ -76,6 +78,16 @@ class MultiExchange<T> implements Cancelable {
static final Logger debug =
Utils.getDebugLogger("MultiExchange"::toString, Utils.DEBUG);
private record RetryContext(Throwable requestFailureCause,
boolean shouldRetry,
AtomicInteger reqAttemptCounter,
boolean shouldResetConnectTimer) {
private static RetryContext doNotRetry(Throwable requestFailureCause) {
return new RetryContext(requestFailureCause, false, null, false);
}
}
private static final AtomicLong IDS = new AtomicLong();
private final HttpRequest userRequest; // the user request
private final HttpRequestImpl request; // a copy of the user request
private final ConnectTimeoutTracker connectTimeout; // null if no timeout
@ -83,12 +95,11 @@ class MultiExchange<T> implements Cancelable {
final HttpResponse.BodyHandler<T> responseHandler;
final HttpClientImpl.DelegatingExecutor executor;
final AtomicInteger attempts = new AtomicInteger();
final long id = IDS.incrementAndGet();
HttpRequestImpl currentreq; // used for retries & redirect
HttpRequestImpl previousreq; // used for retries & redirect
Exchange<T> exchange; // the current exchange
Exchange<T> previous;
volatile Throwable retryCause;
volatile boolean retriedOnce;
volatile HttpResponse<T> response;
// Maximum number of times a request will be retried/redirected
@ -98,6 +109,12 @@ class MultiExchange<T> implements Cancelable {
"jdk.httpclient.redirects.retrylimit", DEFAULT_MAX_ATTEMPTS
);
// Maximum number of times a request should be retried when
// max streams limit is reached
static final int max_stream_limit_attempts = Utils.getIntegerNetProperty(
"jdk.httpclient.retryOnStreamlimit", max_attempts
);
private final List<HeaderFilter> filters;
volatile ResponseTimerEvent responseTimerEvent;
volatile boolean cancelled;
@ -113,20 +130,22 @@ class MultiExchange<T> implements Cancelable {
volatile AuthenticationFilter.AuthInfo serverauth, proxyauth;
// RedirectHandler
volatile int numberOfRedirects = 0;
// StreamLimit
private final AtomicInteger streamLimitRetries = new AtomicInteger();
// This class is used to keep track of the connection timeout
// across retries, when a ConnectException causes a retry.
// In that case - we will retry the connect, but we don't
// want to double the timeout by starting a new timer with
// the full connectTimeout again.
// Instead we use the ConnectTimeoutTracker to return a new
// Instead, we use the ConnectTimeoutTracker to return a new
// duration that takes into account the time spent in the
// first connect attempt.
// If however, the connection gets connected, but we later
// retry the whole operation, then we reset the timer before
// retrying (since the connection used for the second request
// will not necessarily be the same: it could be a new
// unconnected connection) - see getExceptionalCF().
// unconnected connection) - see checkRetryEligible().
private static final class ConnectTimeoutTracker {
final Duration max;
final AtomicLong startTime = new AtomicLong();
@ -199,8 +218,22 @@ class MultiExchange<T> implements Cancelable {
HttpClient.Version version() {
HttpClient.Version vers = request.version().orElse(client.version());
if (vers == HttpClient.Version.HTTP_2 && !request.secure() && request.proxy() != null)
if (vers != Version.HTTP_1_1
&& !request.secure() && request.proxy() != null
&& !request.isHttp3Only(vers)) {
// downgrade to HTTP_1_1 unless HTTP_3_URI_ONLY.
// if HTTP_3_URI_ONLY and not secure it will fail down the road, so
// we don't downgrade here.
vers = HttpClient.Version.HTTP_1_1;
}
if (vers == Version.HTTP_3 && request.secure() && !client.client3().isPresent()) {
if (!request.isHttp3Only(vers)) {
// HTTP/3 not supported with the client config.
// Downgrade to HTTP/2, unless HTTP_3_URI_ONLY is specified
vers = Version.HTTP_2;
if (debug.on()) debug.log("HTTP_3 downgraded to " + vers);
}
}
return vers;
}
@ -229,28 +262,28 @@ class MultiExchange<T> implements Cancelable {
}
private void requestFilters(HttpRequestImpl r) throws IOException {
Log.logTrace("Applying request filters");
if (Log.trace()) Log.logTrace("Applying request filters");
for (HeaderFilter filter : filters) {
Log.logTrace("Applying {0}", filter);
if (Log.trace()) Log.logTrace("Applying {0}", filter);
filter.request(r, this);
}
Log.logTrace("All filters applied");
if (Log.trace()) Log.logTrace("All filters applied");
}
private HttpRequestImpl responseFilters(Response response) throws IOException
{
Log.logTrace("Applying response filters");
if (Log.trace()) Log.logTrace("Applying response filters");
ListIterator<HeaderFilter> reverseItr = filters.listIterator(filters.size());
while (reverseItr.hasPrevious()) {
HeaderFilter filter = reverseItr.previous();
Log.logTrace("Applying {0}", filter);
if (Log.trace()) Log.logTrace("Applying {0}", filter);
HttpRequestImpl newreq = filter.response(response);
if (newreq != null) {
Log.logTrace("New request: stopping filters");
if (Log.trace()) Log.logTrace("New request: stopping filters");
return newreq;
}
}
Log.logTrace("All filters applied");
if (Log.trace()) Log.logTrace("All filters applied");
return null;
}
@ -293,9 +326,13 @@ class MultiExchange<T> implements Cancelable {
return true;
} else {
if (cancelled) {
debug.log("multi exchange already cancelled: " + interrupted.get());
if (debug.on()) {
debug.log("multi exchange already cancelled: " + interrupted.get());
}
} else {
debug.log("multi exchange mayInterruptIfRunning=" + mayInterruptIfRunning);
if (debug.on()) {
debug.log("multi exchange mayInterruptIfRunning=" + mayInterruptIfRunning);
}
}
}
return false;
@ -316,7 +353,7 @@ class MultiExchange<T> implements Cancelable {
// and therefore doesn't have to include header information which indicates no
// body is present. This is distinct from responses that also do not contain
// response bodies (possibly ever) but which are required to have content length
// info in the header (eg 205). Those cases do not have to be handled specially
// info in the header (e.g. 205). Those cases do not have to be handled specially
private static boolean bodyNotPermitted(Response r) {
return r.statusCode == 204;
@ -344,19 +381,27 @@ class MultiExchange<T> implements Cancelable {
if (exception != null)
result.completeExceptionally(exception);
else {
this.response =
new HttpResponseImpl<>(r.request(), r, this.response, nullBody, exch);
result.complete(this.response);
result.complete(setNewResponse(r.request(), r, nullBody, exch));
}
});
// ensure that the connection is closed or returned to the pool.
return result.whenComplete(exch::nullBody);
}
// creates a new HttpResponseImpl object and assign it to this.response
private HttpResponse<T> setNewResponse(HttpRequest request, Response r, T body, Exchange<T> exch) {
HttpResponse<T> previousResponse = this.response;
return this.response = new HttpResponseImpl<>(request, r, previousResponse, body, exch);
}
private CompletableFuture<HttpResponse<T>>
responseAsync0(CompletableFuture<Void> start) {
return start.thenCompose( v -> responseAsyncImpl())
.thenCompose((Response r) -> {
return start.thenCompose( _ -> {
// this is the first attempt to have the request processed by the server
attempts.set(1);
return responseAsyncImpl(true);
}).thenCompose((Response r) -> {
processAltSvcHeader(r, client(), currentreq);
Exchange<T> exch = getExchange();
if (bodyNotPermitted(r)) {
if (bodyIsPresent(r)) {
@ -368,15 +413,11 @@ class MultiExchange<T> implements Cancelable {
return handleNoBody(r, exch);
}
return exch.readBodyAsync(responseHandler)
.thenApply((T body) -> {
this.response =
new HttpResponseImpl<>(r.request(), r, this.response, body, exch);
return this.response;
});
.thenApply((T body) -> setNewResponse(r.request, r, body, exch));
}).exceptionallyCompose(this::whenCancelled);
}
// returns a CancellationExcpetion that wraps the given cause
// returns a CancellationException that wraps the given cause
// if cancel(boolean) was called, the given cause otherwise
private Throwable wrapIfCancelled(Throwable cause) {
CancellationException interrupt = interrupted.get();
@ -412,79 +453,100 @@ class MultiExchange<T> implements Cancelable {
}
}
private CompletableFuture<Response> responseAsyncImpl() {
CompletableFuture<Response> cf;
if (attempts.incrementAndGet() > max_attempts) {
cf = failedFuture(new IOException("Too many retries", retryCause));
} else {
if (currentreq.timeout().isPresent()) {
responseTimerEvent = ResponseTimerEvent.of(this);
client.registerTimer(responseTimerEvent);
}
try {
// 1. apply request filters
// if currentreq == previousreq the filters have already
// been applied once. Applying them a second time might
// cause some headers values to be added twice: for
// instance, the same cookie might be added again.
if (currentreq != previousreq) {
requestFilters(currentreq);
}
} catch (IOException e) {
return failedFuture(e);
}
Exchange<T> exch = getExchange();
// 2. get response
cf = exch.responseAsync()
.thenCompose((Response response) -> {
HttpRequestImpl newrequest;
try {
// 3. apply response filters
newrequest = responseFilters(response);
} catch (Throwable t) {
IOException e = t instanceof IOException io ? io : new IOException(t);
exch.exchImpl.cancel(e);
return failedFuture(e);
}
// 4. check filter result and repeat or continue
if (newrequest == null) {
if (attempts.get() > 1) {
Log.logError("Succeeded on attempt: " + attempts);
}
return completedFuture(response);
} else {
cancelTimer();
this.response =
new HttpResponseImpl<>(currentreq, response, this.response, null, exch);
Exchange<T> oldExch = exch;
if (currentreq.isWebSocket()) {
// need to close the connection and open a new one.
exch.exchImpl.connection().close();
}
return exch.ignoreBody().handle((r,t) -> {
previousreq = currentreq;
currentreq = newrequest;
retriedOnce = false;
setExchange(new Exchange<>(currentreq, this));
return responseAsyncImpl();
}).thenCompose(Function.identity());
} })
.handle((response, ex) -> {
// 5. handle errors and cancel any timer set
cancelTimer();
if (ex == null) {
assert response != null;
return completedFuture(response);
}
// all exceptions thrown are handled here
CompletableFuture<Response> errorCF = getExceptionalCF(ex, exch.exchImpl);
if (errorCF == null) {
return responseAsyncImpl();
} else {
return errorCF;
} })
.thenCompose(Function.identity());
// we call this only when a request is being retried
private CompletableFuture<Response> retryRequest() {
// maintain state indicating a request being retried
previousreq = currentreq;
// request is being retried, so the filters have already
// been applied once. Applying them a second time might
// cause some headers values to be added twice: for
// instance, the same cookie might be added again.
final boolean applyReqFilters = false;
return responseAsyncImpl(applyReqFilters);
}
private CompletableFuture<Response> responseAsyncImpl(final boolean applyReqFilters) {
if (currentreq.timeout().isPresent()) {
responseTimerEvent = ResponseTimerEvent.of(this);
client.registerTimer(responseTimerEvent);
}
try {
// 1. apply request filters
if (applyReqFilters) {
requestFilters(currentreq);
}
} catch (IOException e) {
return failedFuture(e);
}
final Exchange<T> exch = getExchange();
// 2. get response
final CompletableFuture<Response> cf = exch.responseAsync()
.thenCompose((Response response) -> {
HttpRequestImpl newrequest;
try {
// 3. apply response filters
newrequest = responseFilters(response);
} catch (Throwable t) {
IOException e = t instanceof IOException io ? io : new IOException(t);
exch.exchImpl.cancel(e);
return failedFuture(e);
}
// 4. check filter result and repeat or continue
if (newrequest == null) {
if (attempts.get() > 1) {
if (Log.requests()) {
Log.logResponse(() -> String.format(
"%s #%s Succeeded on attempt %s: statusCode=%s",
request, id, attempts, response.statusCode));
}
}
return completedFuture(response);
} else {
cancelTimer();
setNewResponse(currentreq, response, null, exch);
if (currentreq.isWebSocket()) {
// need to close the connection and open a new one.
exch.exchImpl.connection().close();
}
return exch.ignoreBody().handle((r,t) -> {
previousreq = currentreq;
currentreq = newrequest;
// this is the first attempt to have the new request
// processed by the server
attempts.set(1);
setExchange(new Exchange<>(currentreq, this));
return responseAsyncImpl(true);
}).thenCompose(Function.identity());
} })
.handle((response, ex) -> {
// 5. handle errors and cancel any timer set
cancelTimer();
if (ex == null) {
assert response != null;
return completedFuture(response);
}
// all exceptions thrown are handled here
final RetryContext retryCtx = checkRetryEligible(ex, exch);
assert retryCtx != null : "retry context is null";
if (retryCtx.shouldRetry()) {
// increment the request attempt counter and retry the request
assert retryCtx.reqAttemptCounter != null : "request attempt counter is null";
final int numAttempt = retryCtx.reqAttemptCounter.incrementAndGet();
if (debug.on()) {
debug.log("Retrying request: " + currentreq + " id: " + id
+ " attempt: " + numAttempt + " due to: "
+ retryCtx.requestFailureCause);
}
// reset the connect timer if necessary
if (retryCtx.shouldResetConnectTimer && this.connectTimeout != null) {
this.connectTimeout.reset();
}
return retryRequest();
} else {
assert retryCtx.requestFailureCause != null : "missing request failure cause";
return MinimalFuture.<Response>failedFuture(retryCtx.requestFailureCause);
} })
.thenCompose(Function.identity());
return cf;
}
@ -492,14 +554,14 @@ class MultiExchange<T> implements Cancelable {
String s = Utils.getNetProperty("jdk.httpclient.enableAllMethodRetry");
if (s == null)
return false;
return s.isEmpty() ? true : Boolean.parseBoolean(s);
return s.isEmpty() || Boolean.parseBoolean(s);
}
private static boolean disableRetryConnect() {
String s = Utils.getNetProperty("jdk.httpclient.disableRetryConnect");
if (s == null)
return false;
return s.isEmpty() ? true : Boolean.parseBoolean(s);
return s.isEmpty() || Boolean.parseBoolean(s);
}
/** True if ALL ( even non-idempotent ) requests can be automatic retried. */
@ -517,7 +579,7 @@ class MultiExchange<T> implements Cancelable {
}
/** Returns true if the given request can be automatically retried. */
private static boolean canRetryRequest(HttpRequest request) {
private static boolean isHttpMethodRetriable(HttpRequest request) {
if (RETRY_ALWAYS)
return true;
if (isIdempotentRequest(request))
@ -534,70 +596,125 @@ class MultiExchange<T> implements Cancelable {
return interrupted.get() != null;
}
private boolean retryOnFailure(Throwable t) {
if (requestCancelled()) return false;
return t instanceof ConnectionExpiredException
|| (RETRY_CONNECT && (t instanceof ConnectException));
}
private Throwable retryCause(Throwable t) {
Throwable cause = t instanceof ConnectionExpiredException ? t.getCause() : t;
return cause == null ? t : cause;
String streamLimitState() {
return id + " attempt:" + streamLimitRetries.get();
}
/**
* Takes a Throwable and returns a suitable CompletableFuture that is
* completed exceptionally, or null.
* This method determines if a failed request can be retried. The returned RetryContext
* will contain the {@linkplain RetryContext#shouldRetry() retry decision} and the
* {@linkplain RetryContext#requestFailureCause() underlying
* cause} (computed out of the given {@code requestFailureCause}) of the request failure.
*
* @param requestFailureCause the exception that caused the request to fail
* @param exchg the Exchange
* @return a non-null RetryContext which contains the result of retry eligibility
*/
private CompletableFuture<Response> getExceptionalCF(Throwable t, ExchangeImpl<?> exchImpl) {
if ((t instanceof CompletionException) || (t instanceof ExecutionException)) {
if (t.getCause() != null) {
t = t.getCause();
private RetryContext checkRetryEligible(final Throwable requestFailureCause,
final Exchange<?> exchg) {
assert requestFailureCause != null : "request failure cause is missing";
assert exchg != null : "exchange cannot be null";
// determine the underlying cause for the request failure
final Throwable t = Utils.getCompletionCause(requestFailureCause);
final Throwable underlyingCause = switch (t) {
case IOException ioe -> {
if (cancelled && !requestCancelled() && !(ioe instanceof HttpTimeoutException)) {
yield toTimeoutException(ioe);
}
yield ioe;
}
default -> {
yield t;
}
};
if (requestCancelled()) {
// request has been cancelled, do not retry
return RetryContext.doNotRetry(underlyingCause);
}
final boolean retryAsUnprocessed = exchImpl != null && exchImpl.isUnprocessedByPeer();
if (cancelled && !requestCancelled() && t instanceof IOException) {
if (!(t instanceof HttpTimeoutException)) {
t = toTimeoutException((IOException)t);
// check if retry limited is reached. if yes then don't retry.
record Limit(int numAttempts, int maxLimit) {
boolean retryLimitReached() {
return Limit.this.numAttempts >= Limit.this.maxLimit;
}
} else if (retryAsUnprocessed || retryOnFailure(t)) {
Throwable cause = retryCause(t);
if (!(t instanceof ConnectException)) {
// we may need to start a new connection, and if so
// we want to start with a fresh connect timeout again.
if (connectTimeout != null) connectTimeout.reset();
if (!retryAsUnprocessed && !canRetryRequest(currentreq)) {
// a (peer) processed request which cannot be retried, fail with
// the original cause
return failedFuture(cause);
}
} // ConnectException: retry, but don't reset the connectTimeout.
// allow the retry mechanism to do its work
retryCause = cause;
if (!retriedOnce) {
if (debug.on()) {
debug.log(t.getClass().getSimpleName()
+ " (async): retrying " + currentreq + " due to: ", t);
}
retriedOnce = true;
// The connection was abruptly closed.
// We return null to retry the same request a second time.
// The request filters have already been applied to the
// currentreq, so we set previousreq = currentreq to
// prevent them from being applied again.
previousreq = currentreq;
return null;
} else {
if (debug.on()) {
debug.log(t.getClass().getSimpleName()
+ " (async): already retried once " + currentreq, t);
}
t = cause;
};
final Limit limit = switch (underlyingCause) {
case StreamLimitException _ -> {
yield new Limit(streamLimitRetries.get(), max_stream_limit_attempts);
}
case ConnectException _ -> {
// for ConnectException (i.e. inability to establish a connection to the server)
// we currently retry the request only once and don't honour the
// "jdk.httpclient.redirects.retrylimit" configuration value.
yield new Limit(attempts.get(), 2);
}
default -> {
yield new Limit(attempts.get(), max_attempts);
}
};
if (limit.retryLimitReached()) {
if (debug.on()) {
debug.log("request already attempted "
+ limit.numAttempts + " times, won't be retried again "
+ currentreq + " " + id, underlyingCause);
}
final var x = underlyingCause instanceof ConnectionExpiredException cee
? cee.getCause() == null ? cee : cee.getCause()
: underlyingCause;
// do not retry anymore
return RetryContext.doNotRetry(x);
}
return failedFuture(t);
return switch (underlyingCause) {
case ConnectException _ -> {
// connection attempt itself failed, so the request hasn't reached the server.
// check if retry on connection failure is enabled, if not then we don't retry
// the request.
if (!RETRY_CONNECT) {
// do not retry
yield RetryContext.doNotRetry(underlyingCause);
}
// OK to retry. Since the failure is due to a connection/stream being unavailable
// we mark the retry context to not allow the connect timer to be reset
// when the retry is actually attempted.
yield new RetryContext(underlyingCause, true, attempts, false);
}
case StreamLimitException sle -> {
// make a note that the stream limit was reached for a particular HTTP version
exchg.streamLimitReached(true);
// OK to retry. Since the failure is due to a connection/stream being unavailable
// we mark the retry context to not allow the connect timer to be reset
// when the retry is actually attempted.
yield new RetryContext(underlyingCause, true, streamLimitRetries, false);
}
case ConnectionExpiredException cee -> {
final Throwable cause = cee.getCause() == null ? cee : cee.getCause();
// check if the request was explicitly marked as unprocessed, in which case
// we retry
if (exchg.isUnprocessedByPeer()) {
// OK to retry and allow for the connect timer to be reset
yield new RetryContext(cause, true, attempts, true);
}
// the request which failed hasn't been marked as unprocessed which implies that
// it could be processed by the server. check if the request's METHOD allows
// for retry.
if (!isHttpMethodRetriable(currentreq)) {
// request METHOD doesn't allow for retry
yield RetryContext.doNotRetry(cause);
}
// OK to retry and allow for the connect timer to be reset
yield new RetryContext(cause, true, attempts, true);
}
default -> {
// some other exception that caused the request to fail.
// we check if the request has been explicitly marked as "unprocessed",
// which implies the server hasn't processed the request and is thus OK to retry.
if (exchg.isUnprocessedByPeer()) {
// OK to retry and allow for resetting the connect timer
yield new RetryContext(underlyingCause, true, attempts, false);
}
// some other cause of failure, do not retry.
yield RetryContext.doNotRetry(underlyingCause);
}
};
}
private HttpTimeoutException toTimeoutException(IOException ioe) {

View File

@ -26,6 +26,7 @@
package jdk.internal.net.http;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Locale;
import java.util.Objects;
@ -132,6 +133,46 @@ public record Origin(String scheme, String host, int port) {
return host + ":" + port;
}
/**
* {@return true if the Origin's scheme is considered secure, else returns false}
*/
boolean isSecure() {
// we consider https to be the only secure scheme
return scheme.equals("https");
}
/**
* {@return Creates and returns an Origin parsed from the ASCII serialized form as defined
* in section 6.2 of RFC-6454}
*
* @param value The value to be parsed
*/
static Origin fromASCIISerializedForm(final String value) throws IllegalArgumentException {
Objects.requireNonNull(value);
try {
final URI uri = new URI(value);
// the ASCII-serialized form contains scheme://host, optionally followed by :port
if (uri.getScheme() == null || uri.getHost() == null) {
throw new IllegalArgumentException("Invalid ASCII serialized form of origin");
}
// normalize the origin string, check if we get the same result
String normalized = uri.getScheme() + "://" + uri.getHost();
if (uri.getPort() != -1) {
normalized += ":" + uri.getPort();
}
if (!value.equals(normalized)) {
throw new IllegalArgumentException("Invalid ASCII serialized form of origin");
}
try {
return Origin.from(uri);
} catch (IllegalArgumentException iae) {
throw new IllegalArgumentException("Invalid ASCII serialized form of origin", iae);
}
} catch (URISyntaxException use) {
throw new IllegalArgumentException("Invalid ASCII serialized form of origin", use);
}
}
private static boolean isValidScheme(final String scheme) {
// only "http" and "https" literals allowed
return "http".equals(scheme) || "https".equals(scheme);

View File

@ -266,24 +266,8 @@ class PlainHttpConnection extends HttpConnection {
try {
this.chan = SocketChannel.open();
chan.configureBlocking(false);
if (debug.on()) {
int bufsize = getSoReceiveBufferSize();
debug.log("Initial receive buffer size is: %d", bufsize);
bufsize = getSoSendBufferSize();
debug.log("Initial send buffer size is: %d", bufsize);
}
if (trySetReceiveBufferSize(client.getReceiveBufferSize())) {
if (debug.on()) {
int bufsize = getSoReceiveBufferSize();
debug.log("Receive buffer size configured: %d", bufsize);
}
}
if (trySetSendBufferSize(client.getSendBufferSize())) {
if (debug.on()) {
int bufsize = getSoSendBufferSize();
debug.log("Send buffer size configured: %d", bufsize);
}
}
Utils.configureChannelBuffers(debug::log, chan,
client.getReceiveBufferSize(), client.getSendBufferSize());
chan.setOption(StandardSocketOptions.TCP_NODELAY, true);
// wrap the channel in a Tube for async reading and writing
tube = new SocketTube(client(), chan, Utils::getBuffer, label);
@ -292,54 +276,6 @@ class PlainHttpConnection extends HttpConnection {
}
}
private int getSoReceiveBufferSize() {
try {
return chan.getOption(StandardSocketOptions.SO_RCVBUF);
} catch (IOException x) {
if (debug.on())
debug.log("Failed to get initial receive buffer size on %s", chan);
}
return 0;
}
private int getSoSendBufferSize() {
try {
return chan.getOption(StandardSocketOptions.SO_SNDBUF);
} catch (IOException x) {
if (debug.on())
debug.log("Failed to get initial receive buffer size on %s", chan);
}
return 0;
}
private boolean trySetReceiveBufferSize(int bufsize) {
try {
if (bufsize > 0) {
chan.setOption(StandardSocketOptions.SO_RCVBUF, bufsize);
return true;
}
} catch (IOException x) {
if (debug.on())
debug.log("Failed to set receive buffer size to %d on %s",
bufsize, chan);
}
return false;
}
private boolean trySetSendBufferSize(int bufsize) {
try {
if (bufsize > 0) {
chan.setOption(StandardSocketOptions.SO_SNDBUF, bufsize);
return true;
}
} catch (IOException x) {
if (debug.on())
debug.log("Failed to set send buffer size to %d on %s",
bufsize, chan);
}
return false;
}
@Override
HttpPublisher publisher() { return writePublisher; }

View File

@ -25,6 +25,7 @@
package jdk.internal.net.http;
import java.net.http.HttpResponse.PushPromiseHandler.PushId;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.net.http.HttpRequest;
@ -105,9 +106,21 @@ class PushGroup<T> {
}
Acceptor<T> acceptPushRequest(HttpRequest pushRequest) {
return doAcceptPushRequest(pushRequest, null);
}
Acceptor<T> acceptPushRequest(HttpRequest pushRequest, PushId pushId) {
return doAcceptPushRequest(pushRequest, Objects.requireNonNull(pushId));
}
private Acceptor<T> doAcceptPushRequest(HttpRequest pushRequest, PushId pushId) {
AcceptorImpl<T> acceptor = new AcceptorImpl<>(executor);
try {
pushPromiseHandler.applyPushPromise(initiatingRequest, pushRequest, acceptor::accept);
if (pushId == null) {
pushPromiseHandler.applyPushPromise(initiatingRequest, pushRequest, acceptor::accept);
} else {
pushPromiseHandler.applyPushPromise(initiatingRequest, pushRequest, pushId, acceptor::accept);
}
} catch (Throwable t) {
if (acceptor.accepted()) {
CompletableFuture<?> cf = acceptor.cf();
@ -128,6 +141,10 @@ class PushGroup<T> {
}
}
void acceptPushPromiseId(PushId pushId) {
pushPromiseHandler.notifyAdditionalPromise(initiatingRequest, pushId);
}
void pushCompleted() {
stateLock.lock();
try {

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2016, 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
@ -25,13 +25,14 @@
package jdk.internal.net.http;
import java.net.URI;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpHeaders;
import java.net.InetSocketAddress;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLSession;
import jdk.internal.net.http.common.Utils;
/**
@ -71,17 +72,14 @@ class Response {
this.statusCode = statusCode;
this.isConnectResponse = isConnectResponse;
if (connection != null) {
InetSocketAddress a;
try {
a = (InetSocketAddress)connection.channel().getLocalAddress();
} catch (IOException e) {
a = null;
}
this.localAddress = a;
if (connection instanceof AbstractAsyncSSLConnection) {
AbstractAsyncSSLConnection cc = (AbstractAsyncSSLConnection)connection;
this.localAddress = revealedLocalSocketAddress(connection);
if (connection instanceof AbstractAsyncSSLConnection cc) {
SSLEngine engine = cc.getEngine();
sslSession = Utils.immutableSession(engine.getSession());
} else if (connection instanceof HttpQuicConnection qc) {
// TODO: consider adding Optional<SSLSession> getSession() to HttpConnection?
var session = qc.quicConnection().getTLSEngine().getSession();
sslSession = Utils.immutableSession(session);
} else {
sslSession = null;
}
@ -128,4 +126,12 @@ class Response {
sb.append(" Local port: ").append(localAddress.getPort());
return sb.toString();
}
private static InetSocketAddress revealedLocalSocketAddress(HttpConnection connection) {
try {
return (InetSocketAddress) connection.channel().getLocalAddress();
} catch (IOException io) {
return null;
}
}
}

View File

@ -526,7 +526,7 @@ public class ResponseSubscribers {
@Override
public void onError(Throwable thrwbl) {
if (debug.on())
debug.log("onError called: " + thrwbl);
debug.log("onError called", thrwbl);
subscription = null;
failed = Objects.requireNonNull(thrwbl);
// The client process that reads the input stream might
@ -1086,6 +1086,16 @@ public class ResponseSubscribers {
bs.getBody().whenComplete((r, t) -> {
if (t != null) {
cf.completeExceptionally(t);
// if a user-provided BodySubscriber returns
// a getBody() CF completed exceptionally, it's
// the responsibility of that BodySubscriber to cancel
// its subscription in order to cancel the request,
// if operations are still in progress.
// Calling the errorHandler here would ensure that the
// request gets cancelled, but there me cases where that is
// not what the caller wants. Therefore, it's better to
// not call `errorHandler.accept(t);` here, but leave it
// to the provided BodySubscriber implementation.
} else {
cf.complete(r);
}

View File

@ -59,6 +59,8 @@ import jdk.internal.net.http.common.*;
import jdk.internal.net.http.frame.*;
import jdk.internal.net.http.hpack.DecodingCallback;
import static jdk.internal.net.http.AltSvcProcessor.processAltSvcFrame;
import static jdk.internal.net.http.Exchange.MAX_NON_FINAL_RESPONSES;
/**
@ -96,8 +98,8 @@ import static jdk.internal.net.http.Exchange.MAX_NON_FINAL_RESPONSES;
* placed on the stream's inputQ which is consumed by the stream's
* reader thread.
*
* PushedStream sub class
* ======================
* PushedStream subclass
* =====================
* Sending side methods are not used because the request comes from a PUSH_PROMISE
* frame sent by the server. When a PUSH_PROMISE is received the PushedStream
* is created. PushedStream does not use responseCF list as there can be only
@ -151,7 +153,7 @@ class Stream<T> extends ExchangeImpl<T> {
// Indicates the first reason that was invoked when sending a ResetFrame
// to the server. A streamState of 0 indicates that no reset was sent.
// (see markStream(int code)
private volatile int streamState; // assigned using STREAM_STATE varhandle.
private volatile int streamState; // assigned while holding the sendLock.
private volatile boolean deRegistered; // assigned using DEREGISTERED varhandle.
// state flags
@ -219,7 +221,7 @@ class Stream<T> extends ExchangeImpl<T> {
List<ByteBuffer> buffers = df.getData();
List<ByteBuffer> dsts = Collections.unmodifiableList(buffers);
int size = Utils.remaining(dsts, Integer.MAX_VALUE);
long size = Utils.remaining(dsts, Long.MAX_VALUE);
if (size == 0 && finished) {
inputQ.remove();
// consumed will not be called
@ -478,7 +480,9 @@ class Stream<T> extends ExchangeImpl<T> {
if (code == 0) return streamState;
sendLock.lock();
try {
return (int) STREAM_STATE.compareAndExchange(this, 0, code);
var state = streamState;
if (state == 0) streamState = code;
return state;
} finally {
sendLock.unlock();
}
@ -534,7 +538,7 @@ class Stream<T> extends ExchangeImpl<T> {
this.requestPublisher = request.requestPublisher; // may be null
this.responseHeadersBuilder = new HttpHeadersBuilder();
this.rspHeadersConsumer = new HeadersConsumer();
this.requestPseudoHeaders = createPseudoHeaders(request);
this.requestPseudoHeaders = Utils.createPseudoHeaders(request);
this.streamWindowUpdater = new StreamWindowUpdateSender(connection);
}
@ -587,6 +591,7 @@ class Stream<T> extends ExchangeImpl<T> {
case WindowUpdateFrame.TYPE -> incoming_windowUpdate((WindowUpdateFrame) frame);
case ResetFrame.TYPE -> incoming_reset((ResetFrame) frame);
case PriorityFrame.TYPE -> incoming_priority((PriorityFrame) frame);
case AltSvcFrame.TYPE -> handleAltSvcFrame(streamid, (AltSvcFrame) frame);
default -> throw new IOException("Unexpected frame: " + frame);
}
@ -745,6 +750,10 @@ class Stream<T> extends ExchangeImpl<T> {
}
}
void handleAltSvcFrame(int streamid, AltSvcFrame asf) {
processAltSvcFrame(streamid, asf, connection.connection, connection.client());
}
void handleReset(ResetFrame frame, Flow.Subscriber<?> subscriber) {
Log.logTrace("Handling RST_STREAM on stream {0}", streamid);
if (!closed) {
@ -763,12 +772,16 @@ class Stream<T> extends ExchangeImpl<T> {
// A REFUSED_STREAM error code implies that the stream wasn't processed by the
// peer and the client is free to retry the request afresh.
if (error == ErrorFrame.REFUSED_STREAM) {
// null exchange implies a PUSH stream and those aren't
// initiated by the client, so we don't expect them to be
// considered unprocessed.
assert this.exchange != null : "PUSH streams aren't expected to be marked as unprocessed";
// Here we arrange for the request to be retried. Note that we don't call
// closeAsUnprocessed() method here because the "closed" state is already set
// to true a few lines above and calling close() from within
// closeAsUnprocessed() will end up being a no-op. We instead do the additional
// bookkeeping here.
markUnprocessedByPeer();
this.exchange.markUnprocessedByPeer();
errorRef.compareAndSet(null, new IOException("request not processed by peer"));
if (debug.on()) {
debug.log("request unprocessed by peer (REFUSED_STREAM) " + this.request);
@ -1216,6 +1229,7 @@ class Stream<T> extends ExchangeImpl<T> {
assert !endStreamSent : "internal error, send data after END_STREAM flag";
}
if ((state = streamState) != 0) {
t = errorRef.get();
if (debug.on()) debug.log("trySend: cancelled: %s", String.valueOf(t));
break;
}
@ -1521,7 +1535,7 @@ class Stream<T> extends ExchangeImpl<T> {
} else cancelImpl(cause);
}
// This method sends a RST_STREAM frame
// This method sends an RST_STREAM frame
void cancelImpl(Throwable e) {
cancelImpl(e, ResetFrame.CANCEL);
}
@ -1856,8 +1870,12 @@ class Stream<T> extends ExchangeImpl<T> {
*/
void closeAsUnprocessed() {
try {
// null exchange implies a PUSH stream and those aren't
// initiated by the client, so we don't expect them to be
// considered unprocessed.
assert this.exchange != null : "PUSH streams aren't expected to be closed as unprocessed";
// We arrange for the request to be retried on a new connection as allowed by the RFC-9113
markUnprocessedByPeer();
this.exchange.markUnprocessedByPeer();
this.errorRef.compareAndSet(null, new IOException("request not processed by peer"));
if (debug.on()) {
debug.log("closing " + this.request + " as unprocessed by peer");
@ -1905,7 +1923,7 @@ class Stream<T> extends ExchangeImpl<T> {
streamid, n, v);
}
} catch (UncheckedIOException uio) {
// reset stream: From RFC 9113, section 8.1
// reset stream: From RFC 7540, section-8.1.2.6
// Malformed requests or responses that are detected MUST be
// treated as a stream error (Section 5.4.2) of type
// PROTOCOL_ERROR.
@ -1953,13 +1971,10 @@ class Stream<T> extends ExchangeImpl<T> {
}
private static final VarHandle STREAM_STATE;
private static final VarHandle DEREGISTERED;
static {
try {
MethodHandles.Lookup lookup = MethodHandles.lookup();
STREAM_STATE = lookup
.findVarHandle(Stream.class, "streamState", int.class);
DEREGISTERED = lookup
.findVarHandle(Stream.class, "deRegistered", boolean.class);
} catch (Exception x) {

View File

@ -34,4 +34,9 @@ public final class Alpns {
public static final String HTTP_1_1 = "http/1.1";
public static final String H2 = "h2";
public static final String H2C = "h2c";
public static final String H3 = "h3";
public static boolean isSecureALPNName(final String alpnName) {
return H3.equals(alpnName) || H2.equals(alpnName);
}
}

View File

@ -29,7 +29,9 @@ import java.io.IOException;
/**
* Signals that an end of file or end of stream has been reached
* unexpectedly before any protocol specific data has been received.
* unexpectedly before any protocol specific data has been received,
* or that a new stream creation was rejected because the underlying
* connection was closed.
*/
public final class ConnectionExpiredException extends IOException {
private static final long serialVersionUID = 0;

View File

@ -88,6 +88,24 @@ public final class Deadline implements Comparable<Deadline> {
return of(deadline.truncatedTo(unit));
}
/**
* Returns a copy of this deadline with the specified amount subtracted.
* <p>
* This returns a {@code Deadline}, based on this one, with the specified amount subtracted.
* The amount is typically {@link Duration} but may be any other type implementing
* the {@link TemporalAmount} interface.
* <p>
* This instance is immutable and unaffected by this method call.
*
* @param amountToSubtract the amount to subtract, not null
* @return a {@code Deadline} based on this deadline with the subtraction made, not null
* @throws DateTimeException if the subtraction cannot be made
* @throws ArithmeticException if numeric overflow occurs
*/
public Deadline minus(TemporalAmount amountToSubtract) {
return Deadline.of(deadline.minus(amountToSubtract));
}
/**
* Returns a copy of this deadline with the specified amount added.
* <p>
@ -126,6 +144,21 @@ public final class Deadline implements Comparable<Deadline> {
return Deadline.of(deadline.plusSeconds(secondsToAdd));
}
/**
* Returns a copy of this deadline with the specified duration in milliseconds added.
* <p>
* This instance is immutable and unaffected by this method call.
*
* @param millisToAdd the milliseconds to add, positive or negative
* @return a {@code Deadline} based on this deadline with the specified milliseconds added, not null
* @throws DateTimeException if the result exceeds the maximum or minimum deadline
* @throws ArithmeticException if numeric overflow occurs
*/
public Deadline plusMillis(long millisToAdd) {
if (millisToAdd == 0) return this;
return Deadline.of(deadline.plusMillis(millisToAdd));
}
/**
* Returns a copy of this deadline with the specified amount added.
* <p>
@ -183,7 +216,7 @@ public final class Deadline implements Comparable<Deadline> {
/**
* Checks if this deadline is before the specified deadline.
* <p>
* The comparison is based on the time-line position of the deadines.
* The comparison is based on the time-line position of the deadlines.
*
* @param otherDeadline the other deadine to compare to, not null
* @return true if this deadline is before the specified deadine
@ -217,7 +250,26 @@ public final class Deadline implements Comparable<Deadline> {
return deadline.hashCode();
}
Instant asInstant() {
return deadline;
}
static Deadline of(Instant instant) {
return new Deadline(instant);
}
/**
* Obtains a {@code Duration} representing the duration between two deadlines.
* <p>
* The result of this method can be a negative period if the end is before the start.
*
* @param startInclusive the start deadline, inclusive, not null
* @param endExclusive the end deadline, exclusive, not null
* @return a {@code Duration}, not null
* @throws DateTimeException if the seconds between the deadline cannot be obtained
* @throws ArithmeticException if the calculation exceeds the capacity of {@code Duration}
*/
public static Duration between(Deadline startInclusive, Deadline endExclusive) {
return Duration.between(startInclusive.deadline, endExclusive.deadline);
}
}

View File

@ -284,6 +284,7 @@ public class HttpBodySubscriberWrapper<T> implements TrustedSubscriber<T> {
*/
public final void complete(Throwable t) {
if (markCompleted()) {
logComplete(t);
tryUnregister();
t = withError = Utils.getCompletionCause(t);
if (t == null) {
@ -312,6 +313,10 @@ public class HttpBodySubscriberWrapper<T> implements TrustedSubscriber<T> {
}
}
protected void logComplete(Throwable error) {
}
/**
* {@return true if this subscriber has already completed, either normally
* or abnormally}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2018, 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
@ -41,6 +41,15 @@ public class HttpHeadersBuilder {
headersMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
}
// used in test library (Http3ServerExchange)
public HttpHeadersBuilder(HttpHeaders headers) {
headersMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
for (Map.Entry<String, List<String>> entry : headers.map().entrySet()) {
List<String> valuesCopy = new ArrayList<>(entry.getValue());
headersMap.put(entry.getKey(), valuesCopy);
}
}
public HttpHeadersBuilder structuralCopy() {
HttpHeadersBuilder builder = new HttpHeadersBuilder();
for (Map.Entry<String, List<String>> entry : headersMap.entrySet()) {

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2015, 2020, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2015, 2023, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -28,14 +28,28 @@ package jdk.internal.net.http.common;
import java.net.http.HttpHeaders;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Stream;
import jdk.internal.net.http.frame.DataFrame;
import jdk.internal.net.http.frame.Http2Frame;
import jdk.internal.net.http.frame.WindowUpdateFrame;
import jdk.internal.net.http.quic.frames.AckFrame;
import jdk.internal.net.http.quic.frames.CryptoFrame;
import jdk.internal.net.http.quic.frames.HandshakeDoneFrame;
import jdk.internal.net.http.quic.frames.PaddingFrame;
import jdk.internal.net.http.quic.frames.PingFrame;
import jdk.internal.net.http.quic.frames.QuicFrame;
import jdk.internal.net.http.quic.frames.StreamFrame;
import jdk.internal.net.http.quic.packets.PacketSpace;
import jdk.internal.net.http.quic.packets.QuicPacket;
import jdk.internal.net.http.quic.packets.QuicPacket.PacketType;
import javax.net.ssl.SNIServerName;
import javax.net.ssl.SSLParameters;
@ -43,7 +57,8 @@ import javax.net.ssl.SSLParameters;
/**
* -Djdk.httpclient.HttpClient.log=
* errors,requests,headers,
* frames[:control:data:window:all..],content,ssl,trace,channel
* frames[:control:data:window:all..],content,ssl,trace,channel,
* quic[:control:processed:retransmit:ack:crypto:data:cc:hs:dbb:ping:all]
*
* Any of errors, requests, headers or content are optional.
*
@ -57,15 +72,17 @@ public abstract class Log implements System.Logger {
static final String logProp = "jdk.httpclient.HttpClient.log";
public static final int OFF = 0;
public static final int ERRORS = 0x1;
public static final int REQUESTS = 0x2;
public static final int HEADERS = 0x4;
public static final int CONTENT = 0x8;
public static final int FRAMES = 0x10;
public static final int SSL = 0x20;
public static final int TRACE = 0x40;
public static final int CHANNEL = 0x80;
public static final int OFF = 0x00;
public static final int ERRORS = 0x01;
public static final int REQUESTS = 0x02;
public static final int HEADERS = 0x04;
public static final int CONTENT = 0x08;
public static final int FRAMES = 0x10;
public static final int SSL = 0x20;
public static final int TRACE = 0x40;
public static final int CHANNEL = 0x80;
public static final int QUIC = 0x0100;
public static final int HTTP3 = 0x0200;
static int logging;
// Frame types: "control", "data", "window", "all"
@ -75,6 +92,27 @@ public abstract class Log implements System.Logger {
public static final int ALL = CONTROL| DATA | WINDOW_UPDATES;
static int frametypes;
// Quic message types
public static final int QUIC_CONTROL = 1;
public static final int QUIC_PROCESSED = 2;
public static final int QUIC_RETRANSMIT = 4;
public static final int QUIC_DATA = 8;
public static final int QUIC_CRYPTO = 16;
public static final int QUIC_ACK = 32;
public static final int QUIC_PING = 64;
public static final int QUIC_CC = 128;
public static final int QUIC_TIMER = 256;
public static final int QUIC_DIRECT_BUFFER_POOL = 512;
public static final int QUIC_HANDSHAKE = 1024;
public static final int QUIC_ALL = QUIC_CONTROL
| QUIC_PROCESSED | QUIC_RETRANSMIT
| QUIC_DATA | QUIC_CRYPTO
| QUIC_ACK | QUIC_PING | QUIC_CC
| QUIC_TIMER | QUIC_DIRECT_BUFFER_POOL
| QUIC_HANDSHAKE;
static int quictypes;
static final System.Logger logger;
static {
@ -94,6 +132,12 @@ public abstract class Log implements System.Logger {
case "headers":
logging |= HEADERS;
break;
case "quic":
logging |= QUIC;
break;
case "http3":
logging |= HTTP3;
break;
case "content":
logging |= CONTENT;
break;
@ -107,13 +151,14 @@ public abstract class Log implements System.Logger {
logging |= TRACE;
break;
case "all":
logging |= CONTENT|HEADERS|REQUESTS|FRAMES|ERRORS|TRACE|SSL| CHANNEL;
logging |= CONTENT | HEADERS | REQUESTS | FRAMES | ERRORS | TRACE | SSL | CHANNEL | QUIC | HTTP3;
frametypes |= ALL;
quictypes |= QUIC_ALL;
break;
default:
// ignore bad values
}
if (val.startsWith("frames")) {
if (val.startsWith("frames:") || val.equals("frames")) {
logging |= FRAMES;
String[] types = val.split(":");
if (types.length == 1) {
@ -139,6 +184,56 @@ public abstract class Log implements System.Logger {
}
}
}
if (val.startsWith("quic:") || val.equals("quic")) {
logging |= QUIC;
String[] types = val.split(":");
if (types.length == 1) {
quictypes = QUIC_ALL & ~QUIC_TIMER & ~QUIC_DIRECT_BUFFER_POOL;
} else {
for (String type : types) {
switch (type.toLowerCase(Locale.US)) {
case "control":
quictypes |= QUIC_CONTROL;
break;
case "data":
quictypes |= QUIC_DATA;
break;
case "processed":
quictypes |= QUIC_PROCESSED;
break;
case "retransmit":
quictypes |= QUIC_RETRANSMIT;
break;
case "crypto":
quictypes |= QUIC_CRYPTO;
break;
case "cc":
quictypes |= QUIC_CC;
break;
case "hs":
quictypes |= QUIC_HANDSHAKE;
break;
case "ack":
quictypes |= QUIC_ACK;
break;
case "ping":
quictypes |= QUIC_PING;
break;
case "timer":
quictypes |= QUIC_TIMER;
break;
case "dbb":
quictypes |= QUIC_DIRECT_BUFFER_POOL;
break;
case "all":
quictypes = QUIC_ALL;
break;
default:
// ignore bad values
}
}
}
}
}
}
if (logging != OFF) {
@ -175,6 +270,119 @@ public abstract class Log implements System.Logger {
return (logging & CHANNEL) != 0;
}
public static boolean altsvc() { return headers(); }
public static boolean quicRetransmit() {
return (logging & QUIC) != 0 && (quictypes & QUIC_RETRANSMIT) != 0;
}
// not called directly - but impacts isLogging(QuicFrame)
public static boolean quicHandshake() {
return (logging & QUIC) != 0 && (quictypes & QUIC_HANDSHAKE) != 0;
}
public static boolean quicProcessed() {
return (logging & QUIC) != 0 && (quictypes & QUIC_PROCESSED) != 0;
}
// not called directly - but impacts isLogging(QuicFrame)
public static boolean quicData() {
return (logging & QUIC) != 0 && (quictypes & QUIC_DATA) != 0;
}
public static boolean quicCrypto() {
return (logging & QUIC) != 0 && (quictypes & QUIC_CRYPTO) != 0;
}
public static boolean quicCC() {
return (logging & QUIC) != 0 && (quictypes & QUIC_CC) != 0;
}
public static boolean quicControl() {
return (logging & QUIC) != 0 && (quictypes & QUIC_CONTROL) != 0;
}
public static boolean quicTimer() {
return (logging & QUIC) != 0 && (quictypes & QUIC_TIMER) != 0;
}
public static boolean quicDBB() {
return (logging & QUIC) != 0 && (quictypes & QUIC_DIRECT_BUFFER_POOL) != 0;
}
public static boolean quic() {
return (logging & QUIC) != 0;
}
public static boolean http3() {
return (logging & HTTP3) != 0;
}
public static void logHttp3(String s, Object... s1) {
if (http3()) {
logger.log(Level.INFO, "HTTP3: " + s, s1);
}
}
private static boolean isLogging(QuicFrame frame) {
if (frame instanceof StreamFrame sf)
return (quictypes & QUIC_DATA) != 0
|| (quictypes & QUIC_CONTROL) != 0 && sf.isLast()
|| (quictypes & QUIC_CONTROL) != 0 && sf.offset() == 0;
if (frame instanceof AckFrame)
return (quictypes & QUIC_ACK) != 0;
if (frame instanceof CryptoFrame)
return (quictypes & QUIC_CRYPTO) != 0
|| (quictypes & QUIC_HANDSHAKE) != 0;
if (frame instanceof PingFrame)
return (quictypes & QUIC_PING) != 0;
if (frame instanceof PaddingFrame) return false;
if (frame instanceof HandshakeDoneFrame && quicHandshake())
return true;
return (quictypes & QUIC_CONTROL) != 0;
}
private static final EnumSet<PacketType> HS_TYPES = EnumSet.complementOf(
EnumSet.of(PacketType.ONERTT));
private static boolean quicPacketLoggable(QuicPacket packet) {
return (logging & QUIC) != 0
&& (quictypes == QUIC_ALL
|| quicHandshake() && HS_TYPES.contains(packet.packetType())
|| stream(packet.frames()).anyMatch(Log::isLogging));
}
public static boolean quicPacketOutLoggable(QuicPacket packet) {
return quicPacketLoggable(packet);
}
private static <T> Stream<T> stream(Collection<T> list) {
return list == null ? Stream.empty() : list.stream();
}
public static boolean quicPacketInLoggable(QuicPacket packet) {
return quicPacketLoggable(packet);
}
public static void logQuic(String s, Object... s1) {
if (quic()) {
logger.log(Level.INFO, "QUIC: " + s, s1);
}
}
public static void logQuicPacketOut(String connectionTag, QuicPacket packet) {
if (quicPacketOutLoggable(packet)) {
logger.log(Level.INFO, "QUIC: {0} OUT: {1}",
connectionTag, packet.prettyPrint());
}
}
public static void logQuicPacketIn(String connectionTag, QuicPacket packet) {
if (quicPacketInLoggable(packet)) {
logger.log(Level.INFO, "QUIC: {0} IN: {1}",
connectionTag, packet.prettyPrint());
}
}
public static void logError(String s, Object... s1) {
if (errors()) {
logger.log(Level.INFO, "ERROR: " + s, s1);
@ -237,6 +445,12 @@ public abstract class Log implements System.Logger {
}
}
public static void logAltSvc(String s, Object... s1) {
if (altsvc()) {
logger.log(Level.INFO, "ALTSVC: " + s, s1);
}
}
public static boolean loggingFrame(Class<? extends Http2Frame> clazz) {
if (frametypes == ALL) {
return true;

View File

@ -55,6 +55,8 @@ public final class OperationTrackers {
long getOutstandingHttpOperations();
// The number of active HTTP/2 streams
long getOutstandingHttp2Streams();
// The number of active HTTP/3 streams
long getOutstandingHttp3Streams();
// The number of active WebSockets
long getOutstandingWebSocketOperations();
// number of TCP connections still opened

View File

@ -25,7 +25,6 @@
package jdk.internal.net.http.common;
import java.time.Instant;
import java.time.InstantSource;
/**
* A {@link TimeLine} based on {@link System#nanoTime()} for the
@ -52,7 +51,7 @@ public final class TimeSource implements TimeLine {
// The use of Integer.MAX_VALUE is arbitrary.
// Any value not too close to Long.MAX_VALUE
// would do.
static final int TIME_WINDOW = Integer.MAX_VALUE;
static final long TIME_WINDOW = Integer.MAX_VALUE;
final Instant first;
final long firstNanos;

View File

@ -37,33 +37,50 @@ import java.io.PrintStream;
import java.io.UncheckedIOException;
import java.lang.System.Logger.Level;
import java.net.ConnectException;
import java.net.Inet6Address;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.StandardSocketOptions;
import java.net.Proxy;
import java.net.URI;
import java.net.http.HttpHeaders;
import java.net.http.HttpRequest;
import java.net.http.HttpTimeoutException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.CancelledKeyException;
import java.nio.channels.NetworkChannel;
import java.nio.channels.SelectionKey;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.text.Normalizer;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HexFormat;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.CancellationException;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.BiConsumer;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
@ -152,6 +169,10 @@ public final class Utils {
return prop.isEmpty() ? true : Boolean.parseBoolean(prop);
}
// A threshold to decide whether to slice or copy.
// see sliceOrCopy
public static final int SLICE_THRESHOLD = 32;
/**
* Allocated buffer size. Must never be higher than 16K. But can be lower
* if smaller allocation units preferred. HTTP/2 mandates that all
@ -169,7 +190,8 @@ public final class Utils {
private static Set<String> getDisallowedHeaders() {
Set<String> headers = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
headers.addAll(Set.of("connection", "content-length", "expect", "host", "upgrade"));
headers.addAll(Set.of("connection", "content-length", "expect", "host", "upgrade",
"alt-used"));
String v = getNetProperty("jdk.httpclient.allowRestrictedHeaders");
if (v != null) {
@ -215,6 +237,56 @@ public final class Utils {
return true;
};
public static <T extends Throwable> T addSuppressed(T x, Throwable suppressed) {
if (x != suppressed && suppressed != null) {
var sup = x.getSuppressed();
if (sup != null && sup.length > 0) {
if (Arrays.asList(sup).contains(suppressed)) {
return x;
}
}
sup = suppressed.getSuppressed();
if (sup != null && sup.length > 0) {
if (Arrays.asList(sup).contains(x)) {
return x;
}
}
x.addSuppressed(suppressed);
}
return x;
}
/**
* {@return a string comparing the given deadline with now, typically
* something like "due since Nms" or "due in Nms"}
*
* @apiNote
* This method recognize deadlines set to Instant.MIN
* and Instant.MAX as special cases meaning "due" and
* "not scheduled".
*
* @param now now
* @param deadline the deadline
*/
public static String debugDeadline(Deadline now, Deadline deadline) {
boolean isDue = deadline.compareTo(now) <= 0;
try {
if (isDue) {
if (deadline.equals(Deadline.MIN)) {
return "due (Deadline.MIN)";
} else {
return "due since " + deadline.until(now, ChronoUnit.MILLIS) + "ms";
}
} else if (deadline.equals(Deadline.MAX)) {
return "not scheduled (Deadline.MAX)";
} else {
return "due in " + now.until(deadline, ChronoUnit.MILLIS) + "ms";
}
} catch (ArithmeticException x) {
return isDue ? "due since too long" : "due in the far future";
}
}
public record ProxyHeaders(HttpHeaders userHeaders, HttpHeaders systemHeaders) {}
public static final BiPredicate<String, String> PROXY_TUNNEL_RESTRICTED() {
@ -346,6 +418,7 @@ public final class Utils {
}
public static String interestOps(SelectionKey key) {
if (key == null) return "null-key";
try {
return describeOps(key.interestOps());
} catch (CancelledKeyException x) {
@ -354,6 +427,7 @@ public final class Utils {
}
public static String readyOps(SelectionKey key) {
if (key == null) return "null-key";
try {
return describeOps(key.readyOps());
} catch (CancelledKeyException x) {
@ -438,6 +512,21 @@ public final class Utils {
return cause;
}
public static IOException toIOException(Throwable cause) {
if (cause == null) return null;
if (cause instanceof CompletionException ce) {
cause = ce.getCause();
} else if (cause instanceof ExecutionException ee) {
cause = ee.getCause();
}
if (cause instanceof IOException io) {
return io;
} else if (cause instanceof UncheckedIOException uio) {
return uio.getCause();
}
return new IOException(cause.getMessage(), cause);
}
public static IOException getIOException(Throwable t) {
if (t instanceof IOException) {
return (IOException) t;
@ -575,6 +664,10 @@ public final class Utils {
return Integer.parseInt(System.getProperty(name, String.valueOf(defaultValue)));
}
public static long getLongProperty(String name, long defaultValue) {
return Long.parseLong(System.getProperty(name, String.valueOf(defaultValue)));
}
public static int getIntegerNetProperty(String property, int min, int max, int defaultValue, boolean log) {
int value = Utils.getIntegerNetProperty(property, defaultValue);
// use default value if misconfigured
@ -755,6 +848,91 @@ public final class Utils {
return remain;
}
//
/**
* Reads as much bytes as possible from the buffer list, and
* write them in the provided {@code data} byte array.
* Returns the number of bytes read and written to the byte array.
* This method advances the position in the byte buffers it reads
* @param bufs A list of byte buffer
* @param data A byte array to write into
* @param offset Where to start writing in the byte array
* @return the amount of bytes read and written to the byte array
*/
public static int read(List<ByteBuffer> bufs, byte[] data, int offset) {
int pos = offset;
for (ByteBuffer buf : bufs) {
if (pos >= data.length) break;
int read = Math.min(buf.remaining(), data.length - pos);
if (read <= 0) continue;
buf.get(data, pos, read);
pos += read;
}
return pos - offset;
}
/**
* Returns the next buffer that has remaining bytes, or null.
* @param iterator an iterator
* @return the next buffer that has remaining bytes, or null
*/
public static ByteBuffer next(Iterator<ByteBuffer> iterator) {
ByteBuffer next = null;
while (iterator.hasNext() && !(next = iterator.next()).hasRemaining());
return next == null || !next.hasRemaining() ? null : next;
}
/**
* Compute the relative consolidated position in bytes at which the two
* input mismatch, or -1 if there is no mismatch.
* @apiNote This method behaves as {@link ByteBuffer#mismatch(ByteBuffer)}.
* @param these a first list of byte buffers
* @param those a second list of byte buffers
* @return the relative consolidated position in bytes at which the two
* input mismatch, or -1L if there is no mismatch.
*/
public static long mismatch(List<ByteBuffer> these, List<ByteBuffer> those) {
if (these.isEmpty()) return those.isEmpty() ? -1 : 0;
if (those.isEmpty()) return 0;
Iterator<ByteBuffer> lefti = these.iterator(), righti = those.iterator();
ByteBuffer left = next(lefti), right = next(righti);
long parsed = 0;
while (left != null || right != null) {
int m = left == null || right == null ? 0 : left.mismatch(right);
if (m == -1) {
parsed = parsed + left.remaining();
assert right.remaining() == left.remaining();
if ((left = next(lefti)) != null) {
if ((right = next(righti)) != null) {
continue;
}
return parsed;
}
return (right = next(righti)) != null ? parsed : -1;
}
if (m == 0) return parsed;
parsed = parsed + m;
if (m < left.remaining()) {
if (m < right.remaining()) {
return parsed;
}
if ((right = next(righti)) != null) {
left = left.slice(m, left.remaining() - m);
continue;
}
return parsed;
}
assert m < right.remaining();
if ((left = next(lefti)) != null) {
right = right.slice(m, right.remaining() - m);
continue;
}
return parsed;
}
return -1L;
}
public static long synchronizedRemaining(List<ByteBuffer> bufs) {
if (bufs == null) return 0L;
synchronized (bufs) {
@ -766,12 +944,13 @@ public final class Utils {
if (bufs == null) return 0;
long remain = 0;
for (ByteBuffer buf : bufs) {
remain += buf.remaining();
if (remain > max) {
int size = buf.remaining();
if (max - remain < size) {
throw new IllegalArgumentException("too many bytes");
}
remain += size;
}
return (int) remain;
return remain;
}
public static int remaining(List<ByteBuffer> bufs, int max) {
@ -783,12 +962,13 @@ public final class Utils {
if (refs == null) return 0;
long remain = 0;
for (ByteBuffer b : refs) {
remain += b.remaining();
if (remain > max) {
int size = b.remaining();
if (max - remain < size) {
throw new IllegalArgumentException("too many bytes");
}
remain += size;
}
return (int) remain;
return remain;
}
public static int remaining(ByteBuffer[] refs, int max) {
@ -834,6 +1014,50 @@ public final class Utils {
return newb;
}
/**
* Creates a slice of a buffer, possibly copying the data instead
* of slicing.
* If the buffer capacity is less than the {@linkplain #SLICE_THRESHOLD
* default slice threshold}, or if the capacity minus the length to slice
* is less than the {@linkplain #SLICE_THRESHOLD threshold}, returns a slice.
* Otherwise, copy so as not to retain a reference to a big buffer
* for a small slice.
* @param src the original buffer
* @param start where to start copying/slicing from src
* @param len how many byte to slice/copy
* @return a new ByteBuffer for the given slice
*/
public static ByteBuffer sliceOrCopy(ByteBuffer src, int start, int len) {
return sliceOrCopy(src, start, len, SLICE_THRESHOLD);
}
/**
* Creates a slice of a buffer, possibly copying the data instead
* of slicing.
* If the buffer capacity minus the length to slice is less than the threshold,
* returns a slice.
* Otherwise, copy so as not to retain a reference to a buffer
* that contains more bytes than needed.
* @param src the original buffer
* @param start where to start copying/slicing from src
* @param len how many byte to slice/copy
* @param threshold a threshold to decide whether to slice or copy
* @return a new ByteBuffer for the given slice
*/
public static ByteBuffer sliceOrCopy(ByteBuffer src, int start, int len, int threshold) {
assert src.hasArray();
int cap = src.array().length;
if (cap - len < threshold) {
return src.slice(start, len);
} else {
byte[] b = new byte[len];
if (len > 0) {
src.get(start, b, 0, len);
}
return ByteBuffer.wrap(b);
}
}
/**
* Get the Charset from the Content-encoding header. Defaults to
* UTF_8
@ -849,7 +1073,9 @@ public final class Utils {
if (value == null) return StandardCharsets.UTF_8;
return Charset.forName(value);
} catch (Throwable x) {
Log.logTrace("Can't find charset in \"{0}\" ({1})", type, x);
if (Log.trace()) {
Log.logTrace("Can't find charset in \"{0}\" ({1})", type, x);
}
return StandardCharsets.UTF_8;
}
}
@ -1078,6 +1304,40 @@ public final class Utils {
}
}
/**
* Creates HTTP/2 HTTP/3 pseudo headers for the given request.
* @param request the request
* @return pseudo headers for that request
*/
public static HttpHeaders createPseudoHeaders(HttpRequest request) {
HttpHeadersBuilder hdrs = new HttpHeadersBuilder();
String method = request.method();
hdrs.setHeader(":method", method);
URI uri = request.uri();
hdrs.setHeader(":scheme", uri.getScheme());
String host = uri.getHost();
int port = uri.getPort();
assert host != null;
if (port != -1) {
hdrs.setHeader(":authority", host + ":" + port);
} else {
hdrs.setHeader(":authority", host);
}
String query = uri.getRawQuery();
String path = uri.getRawPath();
if (path == null || path.isEmpty()) {
if (method.equalsIgnoreCase("OPTIONS")) {
path = "*";
} else {
path = "/";
}
}
if (query != null) {
path += "?" + query;
}
hdrs.setHeader(":path", Utils.encode(path));
return hdrs.build();
}
// -- toAsciiString-like support to encode path and query URI segments
// Encodes all characters >= \u0080 into escaped, normalized UTF-8 octets,
@ -1121,6 +1381,302 @@ public final class Utils {
return sb.toString();
}
/**
* {@return the content of the buffer as an hexadecimal string}
* This method doesn't move the buffer position or limit.
* @param buffer a byte buffer
*/
public static String asHexString(ByteBuffer buffer) {
if (!buffer.hasRemaining()) return "";
byte[] bytes = new byte[buffer.remaining()];
buffer.get(buffer.position(), bytes);
return HexFormat.of().formatHex(bytes);
}
/**
* Converts a ByteBuffer containing bytes encoded using
* the given {@linkplain Charset charset} into a
* string. This method does not throw but will replace
* unrecognized sequences with the replacement character.
* The bytes in the buffer are consumed.
*
* @apiNote
* This method is intended for debugging purposes only,
* since buffers are not guaranteed to be split at character
* boundaries.
*
* @param buffer a buffer containing bytes encoded using
* a charset
* @param charset the charset to use to decode the bytes
* into a string
*
* @return a string built from the bytes contained
* in the buffer decoded using the given charset
*/
public static String asString(ByteBuffer buffer, Charset charset) {
var decoded = charset.decode(buffer);
char[] chars = new char[decoded.length()];
decoded.get(chars);
return new String(chars);
}
/**
* Converts a ByteBuffer containing UTF-8 bytes into a
* string. This method does not throw but will replace
* unrecognized sequences with the replacement character.
* The bytes in the buffer are consumed.
*
* @apiNote
* This method is intended for debugging purposes only,
* since buffers are not guaranteed to be split at character
* boundaries.
*
* @param buffer a buffer containing UTF-8 bytes
*
* @return a string built from the decoded UTF-8 bytes contained
* in the buffer
*/
public static String asString(ByteBuffer buffer) {
return asString(buffer, StandardCharsets.UTF_8);
}
public static String millis(Instant now, Instant deadline) {
if (Instant.MAX.equals(deadline)) return "not scheduled";
try {
long delay = now.until(deadline, ChronoUnit.MILLIS);
return delay + " ms";
} catch (ArithmeticException a) {
return "too far away";
}
}
public static String millis(Deadline now, Deadline deadline) {
return millis(now.asInstant(), deadline.asInstant());
}
public static ExecutorService safeExecutor(ExecutorService delegate,
BiConsumer<Runnable, Throwable> errorHandler) {
Executor overflow = new CompletableFuture<Void>().defaultExecutor();
return new SafeExecutorService(delegate, overflow, errorHandler);
}
public static sealed class SafeExecutor<E extends Executor> implements Executor
permits SafeExecutorService {
final E delegate;
final BiConsumer<Runnable, Throwable> errorHandler;
final Executor overflow;
public SafeExecutor(E delegate, Executor overflow, BiConsumer<Runnable, Throwable> errorHandler) {
this.delegate = delegate;
this.overflow = overflow;
this.errorHandler = errorHandler;
}
@Override
public void execute(Runnable command) {
ensureExecutedAsync(command);
}
private void ensureExecutedAsync(Runnable command) {
try {
delegate.execute(command);
} catch (RejectedExecutionException t) {
errorHandler.accept(command, t);
overflow.execute(command);
}
}
}
public static final class SafeExecutorService extends SafeExecutor<ExecutorService>
implements ExecutorService {
public SafeExecutorService(ExecutorService delegate,
Executor overflow,
BiConsumer<Runnable, Throwable> errorHandler) {
super(delegate, overflow, errorHandler);
}
@Override
public void shutdown() {
delegate.shutdown();
}
@Override
public List<Runnable> shutdownNow() {
return delegate.shutdownNow();
}
@Override
public boolean isShutdown() {
return delegate.isShutdown();
}
@Override
public boolean isTerminated() {
return delegate.isTerminated();
}
@Override
public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
return delegate.awaitTermination(timeout, unit);
}
@Override
public <T> Future<T> submit(Callable<T> task) {
return delegate.submit(task);
}
@Override
public <T> Future<T> submit(Runnable task, T result) {
return delegate.submit(task, result);
}
@Override
public Future<?> submit(Runnable task) {
return delegate.submit(task);
}
@Override
public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
throws InterruptedException {
return delegate.invokeAll(tasks);
}
@Override
public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException {
return delegate.invokeAll(tasks, timeout, unit);
}
@Override
public <T> T invokeAny(Collection<? extends Callable<T>> tasks)
throws InterruptedException, ExecutionException {
return delegate.invokeAny(tasks);
}
@Override
public <T> T invokeAny(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException {
return delegate.invokeAny(tasks);
}
}
public static <T extends NetworkChannel> T configureChannelBuffers(Consumer<String> logSink, T chan,
int receiveBufSize, int sendBufSize) {
if (logSink != null) {
int bufsize = getSoReceiveBufferSize(logSink, chan);
logSink.accept("Initial receive buffer size is: %d".formatted(bufsize));
bufsize = getSoSendBufferSize(logSink, chan);
logSink.accept("Initial send buffer size is: %d".formatted(bufsize));
}
if (trySetReceiveBufferSize(logSink, chan, receiveBufSize)) {
if (logSink != null) {
int bufsize = getSoReceiveBufferSize(logSink, chan);
logSink.accept("Receive buffer size configured: %d".formatted(bufsize));
}
}
if (trySetSendBufferSize(logSink, chan, sendBufSize)) {
if (logSink != null) {
int bufsize = getSoSendBufferSize(logSink, chan);
logSink.accept("Send buffer size configured: %d".formatted(bufsize));
}
}
return chan;
}
public static boolean trySetReceiveBufferSize(Consumer<String> logSink, NetworkChannel chan, int bufsize) {
try {
if (bufsize > 0) {
chan.setOption(StandardSocketOptions.SO_RCVBUF, bufsize);
return true;
}
} catch (IOException x) {
if (logSink != null)
logSink.accept("Failed to set receive buffer size to %d on %s"
.formatted(bufsize, chan));
}
return false;
}
public static boolean trySetSendBufferSize(Consumer<String> logSink, NetworkChannel chan, int bufsize) {
try {
if (bufsize > 0) {
chan.setOption(StandardSocketOptions.SO_SNDBUF, bufsize);
return true;
}
} catch (IOException x) {
if (logSink != null)
logSink.accept("Failed to set send buffer size to %d on %s"
.formatted(bufsize, chan));
}
return false;
}
public static int getSoReceiveBufferSize(Consumer<String> logSink, NetworkChannel chan) {
try {
return chan.getOption(StandardSocketOptions.SO_RCVBUF);
} catch (IOException x) {
if (logSink != null)
logSink.accept("Failed to get initial receive buffer size on %s".formatted(chan));
}
return 0;
}
public static int getSoSendBufferSize(Consumer<String> logSink, NetworkChannel chan) {
try {
return chan.getOption(StandardSocketOptions.SO_SNDBUF);
} catch (IOException x) {
if (logSink!= null)
logSink.accept("Failed to get initial receive buffer size on %s".formatted(chan));
}
return 0;
}
/**
* Try to figure out whether local and remote addresses are compatible.
* Used to diagnose potential communication issues early.
* This is a best effort, and there is no guarantee that all potential
* conflicts will be detected.
* @param local local address
* @param peer peer address
* @return a message describing the conflict, if any, or {@code null} if no
* conflict was detected.
*/
public static String addressConflict(SocketAddress local, SocketAddress peer) {
if (local == null || peer == null) return null;
if (local.equals(peer)) {
return "local endpoint and remote endpoint are bound to the same IP address and port";
}
if (!(local instanceof InetSocketAddress li) || !(peer instanceof InetSocketAddress pi)) {
return null;
}
var laddr = li.getAddress();
var paddr = pi.getAddress();
if (!laddr.isAnyLocalAddress() && !paddr.isAnyLocalAddress()) {
if (laddr.getClass() != paddr.getClass()) { // IPv4 vs IPv6
if ((laddr instanceof Inet6Address laddr6 && !laddr6.isIPv4CompatibleAddress())
|| (paddr instanceof Inet6Address paddr6 && !paddr6.isIPv4CompatibleAddress())) {
return "local endpoint IP (%s) and remote endpoint IP (%s) don't match"
.formatted(laddr.getClass().getSimpleName(),
paddr.getClass().getSimpleName());
}
}
}
if (li.getPort() != pi.getPort()) return null;
if (li.getAddress().isAnyLocalAddress() && pi.getAddress().isLoopbackAddress()) {
return "local endpoint (wildcard) and remote endpoint (loopback) ports conflict";
}
if (pi.getAddress().isAnyLocalAddress() && li.getAddress().isLoopbackAddress()) {
return "local endpoint (loopback) and remote endpoint (wildcard) ports conflict";
}
return null;
}
/**
* {@return the exception the given {@code cf} was completed with,
* or a {@link CancellationException} if the given {@code cf} was

View File

@ -0,0 +1,77 @@
/*
* 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.frame;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.Optional;
public final class AltSvcFrame extends Http2Frame {
public static final int TYPE = 0xa;
private final int length;
private final String origin;
private final String altSvcValue;
private static final Charset encoding = StandardCharsets.US_ASCII;
// Strings should be US-ASCII. This is checked by the FrameDecoder.
public AltSvcFrame(int streamid, int flags, Optional<String> originVal, String altValue) {
super(streamid, flags);
this.origin = originVal.orElse("");
this.altSvcValue = Objects.requireNonNull(altValue);
this.length = 2 + origin.length() + altValue.length();
assert origin.length() == origin.getBytes(encoding).length;
assert altSvcValue.length() == altSvcValue.getBytes(encoding).length;
}
@Override
public int type() {
return TYPE;
}
@Override
int length() {
return length;
}
public String getOrigin() {
return origin;
}
public String getAltSvcValue() {
return altSvcValue;
}
@Override
public String toString() {
return super.toString()
+ ", origin=" + this.origin
+ ", alt-svc: " + altSvcValue;
}
}

View File

@ -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,11 +26,13 @@
package jdk.internal.net.http.frame;
import java.io.IOException;
import java.lang.System.Logger.Level;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import jdk.internal.net.http.common.Log;
import jdk.internal.net.http.common.Logger;
import jdk.internal.net.http.common.Utils;
@ -344,6 +346,8 @@ public class FramesDecoder {
return parseWindowUpdateFrame(frameLength, frameStreamid, frameFlags);
case ContinuationFrame.TYPE:
return parseContinuationFrame(frameLength, frameStreamid, frameFlags);
case AltSvcFrame.TYPE:
return parseAltSvcFrame(frameLength, frameStreamid, frameFlags);
default:
// RFC 7540 4.1
// Implementations MUST ignore and discard any frame that has a type that is unknown.
@ -557,4 +561,32 @@ public class FramesDecoder {
return new ContinuationFrame(streamid, flags, getBuffers(false, frameLength));
}
private Http2Frame parseAltSvcFrame(int frameLength, int frameStreamid, int frameFlags) {
var len = getShort();
byte[] origin;
Optional<String> originUri = Optional.empty();
if (len > 0) {
origin = getBytes(len);
if (!isUSAscii(origin)) {
return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR, frameStreamid,
"illegal character in AltSvcFrame");
}
originUri = Optional.of(new String(origin, StandardCharsets.US_ASCII));
}
byte[] altbytes = getBytes(frameLength - 2 - len);
if (!isUSAscii(altbytes)) {
return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR, frameStreamid,
"illegal character in AltSvcFrame");
}
String altSvc = new String(altbytes, StandardCharsets.US_ASCII);
return new AltSvcFrame(frameStreamid, 0, originUri, altSvc);
}
static boolean isUSAscii(byte[] bytes) {
for (int i=0; i < bytes.length; i++) {
if (bytes[i] < 0) return false;
}
return true;
}
}

View File

@ -26,6 +26,7 @@
package jdk.internal.net.http.frame;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
@ -70,6 +71,7 @@ public class FramesEncoder {
case GoAwayFrame.TYPE -> encodeGoAwayFrame((GoAwayFrame) frame);
case WindowUpdateFrame.TYPE -> encodeWindowUpdateFrame((WindowUpdateFrame) frame);
case ContinuationFrame.TYPE -> encodeContinuationFrame((ContinuationFrame) frame);
case AltSvcFrame.TYPE -> encodeAltSvcFrame((AltSvcFrame) frame);
default -> throw new UnsupportedOperationException("Not supported frame " + frame.type() + " (" + frame.getClass().getName() + ")");
};
@ -227,6 +229,20 @@ public class FramesEncoder {
return join(buf, frame.getHeaderBlock());
}
private List<ByteBuffer> encodeAltSvcFrame(AltSvcFrame frame) {
final int length = frame.length();
ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + length);
putHeader(buf, length, AltSvcFrame.TYPE, NO_FLAGS, frame.streamid);
final String origin = frame.getOrigin();
assert (origin.length() & 0xffff0000) == 0;
buf.putShort((short)origin.length());
if (!origin.isEmpty())
buf.put(frame.getOrigin().getBytes(StandardCharsets.US_ASCII));
buf.put(frame.getAltSvcValue().getBytes(StandardCharsets.US_ASCII));
buf.flip();
return List.of(buf);
}
private List<ByteBuffer> joinWithPadding(ByteBuffer buf, List<ByteBuffer> data, int padLength) {
int len = data.size();
if (len == 0) return List.of(buf, getPadding(padLength));

View File

@ -91,8 +91,9 @@ public abstract class Http2Frame {
case PingFrame.TYPE -> "PING";
case PushPromiseFrame.TYPE -> "PUSH_PROMISE";
case WindowUpdateFrame.TYPE -> "WINDOW_UPDATE";
case AltSvcFrame.TYPE -> "ALTSVC";
default -> "UNKNOWN";
default -> "UNKNOWN";
};
}

View File

@ -282,7 +282,7 @@ public final class Decoder {
if (endOfHeaderBlock && state != State.READY) {
logger.log(NORMAL, () -> format("unexpected end of %s representation",
state));
throw new IOException("Unexpected end of header block");
throw new ProtocolException("Unexpected end of header block");
}
if (endOfHeaderBlock) {
size = indexed = 0;

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2015, 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
@ -40,9 +40,10 @@ import java.nio.ByteBuffer;
//
// The encoding is simple and well known: 1 byte <-> 1 char
//
final class ISO_8859_1 {
public final class ISO_8859_1 {
private ISO_8859_1() { }
private ISO_8859_1() {
}
public static final class Reader {

View File

@ -619,7 +619,7 @@ public final class QuickHuffman {
}
}
static final class Reader implements Huffman.Reader {
public static final class Reader implements Huffman.Reader {
private final BufferUpdateConsumer UPDATER =
(buf, bufLen) -> {
@ -703,7 +703,7 @@ public final class QuickHuffman {
}
}
static final class Writer implements Huffman.Writer {
public static final class Writer implements Huffman.Writer {
private final BufferUpdateConsumer UPDATER =
(buf, bufLen) -> {
@ -782,12 +782,26 @@ public final class QuickHuffman {
@Override
public int lengthOf(CharSequence value, int start, int end) {
int len = 0;
for (int i = start; i < end; i++) {
char c = value.charAt(i);
len += codeLengthOf(c);
}
return bytesForBits(len);
return QuickHuffman.lengthOf(value, start, end);
}
}
public static int lengthOf(CharSequence value, int start, int end) {
int len = 0;
for (int i = start; i < end; i++) {
char c = value.charAt(i);
len += codeLengthOf(c);
}
return bytesForBits(len);
}
public static int lengthOf(CharSequence value) {
return lengthOf(value, 0, value.length());
}
/* Used to calculate the number of bytes required for Huffman encoding */
public static boolean isHuffmanBetterFor(CharSequence input) {
return lengthOf(input) < input.length();
}
}

View File

@ -0,0 +1,63 @@
/*
* 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.http3;
import java.util.Objects;
import jdk.internal.net.http.http3.frames.SettingsFrame;
/**
* Represents the settings that are conveyed in a HTTP3 SETTINGS frame for a HTTP3 connection
*/
public record ConnectionSettings(
long maxFieldSectionSize,
long qpackMaxTableCapacity,
long qpackBlockedStreams) {
// we use -1 (an internal value) to represent unlimited
public static final long UNLIMITED_MAX_FIELD_SECTION_SIZE = -1;
public static ConnectionSettings createFrom(final SettingsFrame frame) {
Objects.requireNonNull(frame);
// default is unlimited as per RFC-9114 section 7.2.4.1
final long maxFieldSectionSize = getOrDefault(frame, SettingsFrame.SETTINGS_MAX_FIELD_SECTION_SIZE,
UNLIMITED_MAX_FIELD_SECTION_SIZE);
// default is zero as per RFC-9204 section 5
final long qpackMaxTableCapacity = getOrDefault(frame, SettingsFrame.SETTINGS_QPACK_MAX_TABLE_CAPACITY, 0);
// default is zero as per RFC-9204, section 5
final long qpackBlockedStreams = getOrDefault(frame, SettingsFrame.SETTINGS_QPACK_BLOCKED_STREAMS, 0);
return new ConnectionSettings(maxFieldSectionSize, qpackMaxTableCapacity, qpackBlockedStreams);
}
private static long getOrDefault(final SettingsFrame frame, final int paramId, final long defaultValue) {
final long val = frame.getParameter(paramId);
if (val == -1) {
return defaultValue;
}
return val;
}
}

View File

@ -0,0 +1,308 @@
/*
* 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.http3;
import java.util.HexFormat;
import java.util.Optional;
import java.util.stream.Stream;
import jdk.internal.net.quic.QuicTransportErrors;
/**
* This enum models HTTP/3 error codes as specified in
* <a href="https://www.rfc-editor.org/rfc/rfc9114.html#name-http-3-error-codes">RFC 9114, Section 8</a>,
* augmented with QPack error codes as specified in
* <a href="https://www.rfc-editor.org/rfc/rfc9204.html#section-6">RFC 9204, Section 6</a>.
*/
public enum Http3Error {
/**
* No error.
* <p>
* From <a href="https://www.rfc-editor.org/rfc/rfc9114.html#section-8.1">
* RFC 9114, Section 8.1</a>:
* <blockquote><pre>{@code
* This is used when the connection or stream
* needs to be closed, but there is no error to signal.
* }</pre></blockquote>
*/
H3_NO_ERROR (0x0100), // 256
/**
* General protocol error.
* <p>
* From <a href="https://www.rfc-editor.org/rfc/rfc9114.html#section-8.1">
* RFC 9114, Section 8.1</a>:
* <blockquote><pre>{@code
* Peer violated protocol requirements in a way that does
* not match a more specific error code, or endpoint declines
* to use the more specific error code.
* }</pre></blockquote>
*/
H3_GENERAL_PROTOCOL_ERROR (0x0101), // 257
/**
* Internal error.
* <p>
* From <a href="https://www.rfc-editor.org/rfc/rfc9114.html#section-8.1">
* RFC 9114, Section 8.1</a>:
* <blockquote><pre>{@code
* An internal error has occurred in the HTTP stack.
* }</pre></blockquote>
*/
H3_INTERNAL_ERROR (0x0102), // 258
/**
* Stream creation error.
* <p>
* From <a href="https://www.rfc-editor.org/rfc/rfc9114.html#section-8.1">
* RFC 9114, Section 8.1</a>:
* <blockquote><pre>{@code
* The endpoint detected that its peer created a stream that
* it will not accept.
* }</pre></blockquote>
*/
H3_STREAM_CREATION_ERROR (0x0103), // 259
/**
* Critical stream closed error.
* <p>
* From <a href="https://www.rfc-editor.org/rfc/rfc9114.html#section-8.1">
* RFC 9114, Section 8.1</a>:
* <blockquote><pre>{@code
* A stream required by the HTTP/3 connection was closed or reset.
* }</pre></blockquote>
*/
H3_CLOSED_CRITICAL_STREAM (0x0104), // 260
/**
* Frame unexpected error.
* <p>
* From <a href="https://www.rfc-editor.org/rfc/rfc9114.html#section-8.1">
* RFC 9114, Section 8.1</a>:
* <blockquote><pre>{@code
* A frame was received that was not permitted in the
* current state or on the current stream.
* }</pre></blockquote>
*/
H3_FRAME_UNEXPECTED (0x0105), // 261
/**
* Frame error.
* <p>
* From <a href="https://www.rfc-editor.org/rfc/rfc9114.html#section-8.1">
* RFC 9114, Section 8.1</a>:
* <blockquote><pre>{@code
* A frame that fails to satisfy layout requirements or with
* an invalid size was received.
* }</pre></blockquote>
*/
H3_FRAME_ERROR (0x0106), // 262
/**
* Excessive load error.
* <p>
* From <a href="https://www.rfc-editor.org/rfc/rfc9114.html#section-8.1">
* RFC 9114, Section 8.1</a>:
* <blockquote><pre>{@code
* The endpoint detected that its peer is exhibiting a behavior
* that might be generating excessive load.
* }</pre></blockquote>
*/
H3_EXCESSIVE_LOAD (0x0107), // 263
/**
* Stream ID or Push ID error.
* <p>
* From <a href="https://www.rfc-editor.org/rfc/rfc9114.html#section-8.1">
* RFC 9114, Section 8.1</a>:
* <blockquote><pre>{@code
* A Stream ID or Push ID was used incorrectly, such as exceeding
* a limit, reducing a limit, or being reused.
* }</pre></blockquote>
*/
H3_ID_ERROR (0x0108), // 264
/**
* Settings error.
* <p>
* From <a href="https://www.rfc-editor.org/rfc/rfc9114.html#section-8.1">
* RFC 9114, Section 8.1</a>:
* <blockquote><pre>{@code
* An endpoint detected an error in the payload of a SETTINGS frame.
* }</pre></blockquote>
*/
H3_SETTINGS_ERROR (0x0109), // 265
/**
* Missing settings error.
* <p>
* From <a href="https://www.rfc-editor.org/rfc/rfc9114.html#section-8.1">
* RFC 9114, Section 8.1</a>:
* <blockquote><pre>{@code
* No SETTINGS frame was received at the beginning of the control
* stream.
* }</pre></blockquote>
*/
H3_MISSING_SETTINGS (0x010a), // 266
/**
* Request rejected error.
* <p>
* From <a href="https://www.rfc-editor.org/rfc/rfc9114.html#section-8.1">
* RFC 9114, Section 8.1</a>:
* <blockquote><pre>{@code
* A server rejected a request without performing any application
* processing.
* }</pre></blockquote>
*/
H3_REQUEST_REJECTED (0x010b), // 267
/**
* Request cancelled error.
* <p>
* From <a href="https://www.rfc-editor.org/rfc/rfc9114.html#section-8.1">
* RFC 9114, Section 8.1</a>:
* <blockquote><pre>{@code
* The request or its response (including pushed response) is
* cancelled.
* }</pre></blockquote>
*/
H3_REQUEST_CANCELLED (0x010c), // 268
/**
* Request incomplete error.
* <p>
* From <a href="https://www.rfc-editor.org/rfc/rfc9114.html#section-8.1">
* RFC 9114, Section 8.1</a>:
* <blockquote><pre>{@code
* The client's stream terminated without containing a
* fully-formed request.
* }</pre></blockquote>
*/
H3_REQUEST_INCOMPLETE (0x010d), //269
/**
* Message error.
* <p>
* From <a href="https://www.rfc-editor.org/rfc/rfc9114.html#section-8.1">
* RFC 9114, Section 8.1</a>:
* <blockquote><pre>{@code
* An HTTP message was malformed and cannot be processed.
* }</pre></blockquote>
*/
H3_MESSAGE_ERROR (0x010e), // 270
/**
* Connect error.
* <p>
* From <a href="https://www.rfc-editor.org/rfc/rfc9114.html#section-8.1">
* RFC 9114, Section 8.1</a>:
* <blockquote><pre>{@code
* The TCP connection established in response to a CONNECT
* request was reset or abnormally closed.
* }</pre></blockquote>
*/
H3_CONNECT_ERROR (0x010f), // 271
/**
* Version fallback error
* <p>
* From <a href="https://www.rfc-editor.org/rfc/rfc9114.html#section-8.1">
* RFC 9114, Section 8.1</a>:
* <blockquote><pre>{@code
* The requested operation cannot be served over HTTP/3.
* The peer should retry over HTTP/1.1.
* }</pre></blockquote>
*/
H3_VERSION_FALLBACK (0x0110), // 272
/**
* QPack decompression error
* <p>
* From <a href="https://www.rfc-editor.org/rfc/rfc9204.html#section-6">
* RFC 9204, Section 6</a>:
* <blockquote><pre>{@code
* The decoder failed to interpret an encoded field section
* and is not able to continue decoding that field section.
* }</pre></blockquote>
*/
QPACK_DECOMPRESSION_FAILED (0x0200), // 512
/**
* Qpack encoder stream error.
* <p>
* From <a href="https://www.rfc-editor.org/rfc/rfc9204.html#section-6">
* RFC 9204, Section 6</a>:
* <blockquote><pre>{@code
* The decoder failed to interpret an encoder instruction
* received on the encoder stream.
* }</pre></blockquote>
*/
QPACK_ENCODER_STREAM_ERROR (0x0201), // 513
/**
* Qpack decoder stream error
* <p>
* From <a href="https://www.rfc-editor.org/rfc/rfc9204.html#section-6">
* RFC 9204, Section 6</a>:
* <blockquote><pre>{@code
* The encoder failed to interpret a decoder instruction
* received on the decoder stream.
* }</pre></blockquote>
*/
QPACK_DECODER_STREAM_ERROR (0x0202); // 514
final long errorCode;
Http3Error(long errorCode) {
this.errorCode = errorCode;
}
public long code() {
return errorCode;
}
public static Optional<Http3Error> fromCode(long code) {
return Stream.of(values()).filter((v) -> v.code() == code)
.findFirst();
}
public static String stringForCode(long code) {
return fromCode(code).map(Http3Error::name).orElse(unknown(code));
}
private static String unknown(long code) {
return "UnknownError(code=0x" + HexFormat.of().withUpperCase().toHexDigits(code) + ")";
}
/**
* {@return true if the given code is {@link Http3Error#H3_NO_ERROR} or equivalent}
* Unknown error codes are treated as equivalent to {@code H3_NO_ERROR}
* @param code an HTTP/3 code error code
*/
public static boolean isNoError(long code) {
return fromCode(code).orElse(H3_NO_ERROR) == Http3Error.H3_NO_ERROR;
}
}

View File

@ -0,0 +1,118 @@
/*
* 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.http3.frames;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.util.Random;
import jdk.internal.net.http.http3.Http3Error;
import jdk.internal.net.http.quic.BuffersReader;
import jdk.internal.net.http.quic.VariableLengthEncoder;
import static jdk.internal.net.http.http3.frames.Http3FrameType.asString;
/**
* Super class for all HTTP/3 frames.
*/
public abstract non-sealed class AbstractHttp3Frame implements Http3Frame {
public static final Random RANDOM = new Random();
final long type;
public AbstractHttp3Frame(long type) {
this.type = type;
}
public final String typeAsString() {
return asString(type());
}
@Override
public long type() {
return type;
}
/**
* Computes the size of this frame. This corresponds to
* the {@linkplain #length()} of the frame's payload, plus the
* size needed to encode this length, plus the size needed to
* encode the frame type.
*
* @return the size of this frame.
*/
public long size() {
var len = length();
return len + VariableLengthEncoder.getEncodedSize(len)
+ VariableLengthEncoder.getEncodedSize(type());
}
public int headersSize() {
var len = length();
return VariableLengthEncoder.getEncodedSize(len)
+ VariableLengthEncoder.getEncodedSize(type());
}
@Override
public long streamingLength() {
return 0;
}
protected static long decodeRequiredType(final BuffersReader reader, final long expectedType) {
final long type = VariableLengthEncoder.decode(reader);
if (type < 0) throw new BufferUnderflowException();
// TODO: throw an exception instead?
assert type == expectedType : "bad frame type: " + type + " expected: " + expectedType;
return type;
}
protected static MalformedFrame checkPayloadSize(long frameType,
BuffersReader reader,
long start,
long length) {
// check position after reading payload
long read = reader.position() - start;
if (length != read) {
reader.position(start + length);
reader.release();
return new MalformedFrame(frameType,
Http3Error.H3_FRAME_ERROR.code(),
"payload length mismatch (length=%s, read=%s)"
.formatted(length, start));
}
assert length == reader.position() - start;
return null;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(typeAsString())
.append(": length=")
.append(length());
return sb.toString();
}
}

View File

@ -0,0 +1,115 @@
/*
* 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.http3.frames;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import jdk.internal.net.http.common.Logger;
import jdk.internal.net.http.http3.Http3Error;
import jdk.internal.net.http.quic.BuffersReader;
import jdk.internal.net.http.quic.VariableLengthEncoder;
/**
* Represents the CANCEL_PUSH HTTP3 frame
*/
public final class CancelPushFrame extends AbstractHttp3Frame {
public static final int TYPE = Http3FrameType.TYPE.CANCEL_PUSH_FRAME;
private final long length;
private final long pushId;
public CancelPushFrame(final long pushId) {
super(Http3FrameType.CANCEL_PUSH.type());
this.pushId = pushId;
// the payload length of this frame
this.length = VariableLengthEncoder.getEncodedSize(this.pushId);
}
// only used when constructing the frame during decoding content over a stream
private CancelPushFrame(final long pushId, final long length) {
super(Http3FrameType.CANCEL_PUSH.type());
this.pushId = pushId;
this.length = length;
}
@Override
public long length() {
return this.length;
}
public long getPushId() {
return pushId;
}
public void writeFrame(final ByteBuffer buf) {
// write the type of the frame
VariableLengthEncoder.encode(buf, this.type);
// write the length of the payload
VariableLengthEncoder.encode(buf, this.length);
// write the push id that needs to be cancelled
VariableLengthEncoder.encode(buf, this.pushId);
}
/**
* This method is expected to be called when the reader
* contains enough bytes to decode the frame.
* @param reader the reader
* @param debug a logger for debugging purposes
* @return the new frame
* @throws BufferUnderflowException if the reader doesn't contain
* enough bytes to decode the frame
*/
static AbstractHttp3Frame decodeFrame(final BuffersReader reader, final Logger debug) {
long position = reader.position();
decodeRequiredType(reader, TYPE);
long length = VariableLengthEncoder.decode(reader);
if (length > reader.remaining() || length < 0) {
reader.position(position);
throw new BufferUnderflowException();
}
// position before reading payload
long start = reader.position();
if (length == 0 || length != VariableLengthEncoder.peekEncodedValueSize(reader, start)) {
// frame length does not match the enclosed pushId
return new MalformedFrame(TYPE, Http3Error.H3_FRAME_ERROR.code(),
"Invalid length in CANCEL_PUSH frame: " + length);
}
long pushId = VariableLengthEncoder.decode(reader);
if (pushId == -1) {
reader.position(position);
throw new BufferUnderflowException();
}
// check position after reading payload
var malformed = checkPayloadSize(TYPE, reader, start, length);
if (malformed != null) return malformed;
reader.release();
return new CancelPushFrame(pushId);
}
}

View File

@ -0,0 +1,60 @@
/*
* 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.http3.frames;
/**
* This class models an HTTP/3 DATA frame.
* @apiNote
* An instance of {@code DataFrame} is used to read or writes
* the frame's type and length. The payload is supposed to be
* read or written directly to the stream on its own, after having
* read or written the frame type and length.
* @see PartialFrame
*/
public final class DataFrame extends PartialFrame {
/**
* The DATA frame type, as defined by HTTP/3
*/
public static final int TYPE = Http3FrameType.TYPE.DATA_FRAME;
private final long length;
/**
* Creates a new HTTP/3 HEADERS frame
*/
public DataFrame(long length) {
super(TYPE, length);
this.length = length;
}
@Override
public long length() {
return length;
}
}

View File

@ -0,0 +1,331 @@
/*
* 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.http3.frames;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.LongPredicate;
import java.util.function.Supplier;
import jdk.internal.net.http.common.Logger;
import jdk.internal.net.http.common.Utils;
import jdk.internal.net.http.quic.streams.QuicStreamReader;
import jdk.internal.net.http.quic.BuffersReader;
import jdk.internal.net.http.quic.BuffersReader.ListBuffersReader;
/**
* A FramesDecoder accumulates buffers until a frame can be
* decoded. It also supports decoding {@linkplain PartialFrame
* partial frames} and {@linkplain #readPayloadBytes() reading
* their payload} incrementally.
* @apiNote
* When the frame decoder {@linkplain #poll() returns} a partial
* frame, the same frame will be returned until its payload has been
* {@linkplain PartialFrame#remaining() fully} {@linkplain #readPayloadBytes()
* read}.
* The caller is supposed to call {@link #readPayloadBytes()} until
* {@link #poll()} returns a different frame. At this point there will be no
* {@linkplain PartialFrame#remaining() remaining} payload bytes to read for
* the previous frame.
* <br>
* The sequence of calls: {@snippet :
* framesDecoder.submit(buffer);
* while ((frame = framesDecoder.poll()) != null) {
* if (frame instanceof PartialFrame partial) {
* var nextPayloadBytes = framesDecoder.readPayloadBytes();
* if (nextPayloadBytes == null || nextPayloadBytes.isEmpty()) {
* // no more data is available at this moment
* break;
* }
* // nextPayloadBytes are the next bytes for the payload
* // of the partial frame
* deliverBytes(partial, nextPayloadBytes);
* } else ...
* // got a full frame...
* }
* }
* makes it possible to incrementally deliver payload bytes for
* a frame - since {@code poll()} will always return the same partial
* frame until all its payload has been read.
*/
public class FramesDecoder {
private final Logger debug = Utils.getDebugLogger(this::dbgTag);
private final ListBuffersReader framesReader = BuffersReader.list();
private final ReentrantLock lock = new ReentrantLock();
private final Supplier<String> dbgTag;
private final LongPredicate isAllowed;
// the current partial frame or null
PartialFrame partialFrame;
boolean eof;
/**
* A new {@code FramesDecoder} that accepts all frames.
* @param dbgTag a debug tag for logging
*/
public FramesDecoder(String dbgTag) {
this(dbgTag, FramesDecoder::allAllowed);
}
/**
* A new {@code FramesDecoder} that accepts only frames
* authorized by the given {@code isAllowed} predicate.
* If a frame is not allowed, a {@link MalformedFrame} is
* returned.
* @param dbgTag a debug tag for logging
*/
public FramesDecoder(String dbgTag, LongPredicate isAllowed) {
this(() -> dbgTag, Objects.requireNonNull(isAllowed));
}
/**
* A new {@code FramesDecoder} that accepts only frames
* authorized by the given {@code isAllowed} predicate.
* If a frame is not allowed, a {@link MalformedFrame} is
* returned.
* @param dbgTag a debug tag for logging
*/
public FramesDecoder(Supplier<String> dbgTag, LongPredicate isAllowed) {
this.dbgTag = dbgTag;
this.isAllowed = Objects.requireNonNull(isAllowed);
}
String dbgTag() { return dbgTag.get(); }
/**
* Submit a new buffer to this frames decoder
* @param buffer a new buffer from the stream
*/
public void submit(ByteBuffer buffer) {
lock.lock();
try {
if (buffer == QuicStreamReader.EOF) {
eof = true;
} else {
framesReader.add(buffer);
}
} finally {
lock.unlock();
}
}
/**
* {@return an {@code Http3Frame}, possibly {@linkplain PartialFrame partial},
* or {@code null} if not enough bytes have been receive to decode (at least
* partially) a frame}
* If a frame is illegal or not allowed, a {@link MalformedFrame} is
* returned. The caller is supposed to {@linkplain #clear() clear} all data
* and proceed to close the connection in that case.
*/
public Http3Frame poll() {
lock.lock();
try {
if (partialFrame != null) {
if (partialFrame.remaining() != 0) {
return partialFrame;
} else partialFrame = null;
}
var frame = Http3Frame.decode(framesReader, this::isAllowed, debug);
if (frame instanceof PartialFrame partial) {
partialFrame = partial;
}
return frame;
} finally {
lock.unlock();
}
}
/**
* {@return the next payload bytes for the current partial frame,
* or {@code null} if no partial frame}
* If EOF has been reached ({@link QuicStreamReader#EOF EOF} was
* {@linkplain #submit(ByteBuffer) submitted}, and all buffers have
* been read, the returned list will contain {@link QuicStreamReader#EOF
* EOF}
*/
public List<ByteBuffer> readPayloadBytes() {
lock.lock();
try {
if (partialFrame == null || partialFrame.remaining() == 0) {
partialFrame = null;
return null;
}
if (eof && !framesReader.hasRemaining()) {
return List.of(QuicStreamReader.EOF);
}
return partialFrame.nextPayloadBytes(framesReader);
} finally {
lock.unlock();
}
}
/**
* {@return true if EOF has been reached and all buffers have been read}
*/
public boolean eof() {
lock.lock();
try {
if (!eof) return false;
if (!framesReader.hasRemaining()) return true;
if (partialFrame != null) {
// still some payload data to read...
if (partialFrame.remaining() > 0) return false;
}
var pos = framesReader.position();
try {
// if there's not enough data to decode a new frame or a new
// partial frame then since no more data will ever come, we do have
// reached EOF. If however, we can read a frame from the remaining
// data in the buffer, then EOF is not reached yet.
// The next call to poll() will return that frame.
var frame = Http3Frame.decode(framesReader, this::isAllowed, debug);
return frame == null;
} finally {
// restore position for the next call to poll.
framesReader.position(pos);
}
} finally {
lock.unlock();
}
}
/**
* {@return true if all buffers have been read}
*/
public boolean clean() {
lock.lock();
try {
if (partialFrame != null) {
// still some payload data to read...
if (partialFrame.remaining() > 0) return false;
}
return !framesReader.hasRemaining();
} finally {
lock.unlock();
}
}
/**
* Clears any unconsumed buffers.
*/
public void clear() {
lock.lock();
try {
partialFrame = null;
framesReader.clear();
} finally {
lock.unlock();
}
}
/**
* Can be overridden by subclasses to avoid parsing a frame
* fully if the frame is not allowed on this stream, or
* according to the stream state.
*
* @implSpec
* This method delegates to the {@linkplain #FramesDecoder(String, LongPredicate)
* predicate} given at construction time. If {@linkplain #FramesDecoder(String)
* no predicate} was given this method returns true.
*
* @param frameType the frame type
* @return true if the frame is allowed
*/
protected boolean isAllowed(long frameType) {
return isAllowed.test(frameType);
}
/**
* A predicate that returns true for all frames types allowed
* on the server->client control stream.
* @param frameType a frame type
* @return whether a frame of this type is allowed on a control stream.
*/
public static boolean isAllowedOnControlStream(long frameType) {
if (frameType == Http3FrameType.DATA.type()) return false;
if (frameType == Http3FrameType.HEADERS.type()) return false;
if (frameType == Http3FrameType.PUSH_PROMISE.type()) return false;
if (frameType == Http3FrameType.MAX_PUSH_ID.type()) return false;
if (Http3FrameType.isIllegalType(frameType)) return false;
return true;
}
/**
* A predicate that returns true for all frames types allowed
* on the client->server control stream.
* @param frameType a frame type
* @return whether a frame of this type is allowed on a control stream.
*/
public static boolean isAllowedOnClientControlStream(long frameType) {
if (frameType == Http3FrameType.DATA.type()) return false;
if (frameType == Http3FrameType.HEADERS.type()) return false;
if (frameType == Http3FrameType.PUSH_PROMISE.type()) return false;
if (Http3FrameType.isIllegalType(frameType)) return false;
return true;
}
/**
* A predicate that returns true for all frames types allowed
* on a request/response stream.
* @param frameType a frame type
* @return whether a frame of this type is allowed on a request/response
* stream.
*/
public static boolean isAllowedOnRequestStream(long frameType) {
if (frameType == Http3FrameType.SETTINGS.type()) return false;
if (frameType == Http3FrameType.CANCEL_PUSH.type()) return false;
if (frameType == Http3FrameType.GOAWAY.type()) return false;
if (frameType == Http3FrameType.MAX_PUSH_ID.type()) return false;
if (Http3FrameType.isIllegalType(frameType)) return false;
return true;
}
/**
* A predicate that returns true for all frames types allowed
* on a push promise stream.
* @param frameType a frame type
* @return whether a frame of this type is allowed on a request/response
* stream.
*/
public static boolean isAllowedOnPromiseStream(long frameType) {
if (frameType == Http3FrameType.SETTINGS.type()) return false;
if (frameType == Http3FrameType.CANCEL_PUSH.type()) return false;
if (frameType == Http3FrameType.GOAWAY.type()) return false;
if (frameType == Http3FrameType.MAX_PUSH_ID.type()) return false;
if (frameType == Http3FrameType.PUSH_PROMISE.type()) return false;
if (Http3FrameType.isIllegalType(frameType)) return false;
return true;
}
private static boolean allAllowed(long frameType) {
return true;
}
}

View File

@ -0,0 +1,123 @@
/*
* 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.http3.frames;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import jdk.internal.net.http.common.Logger;
import jdk.internal.net.http.http3.Http3Error;
import jdk.internal.net.http.quic.BuffersReader;
import jdk.internal.net.http.quic.VariableLengthEncoder;
/**
* Represents a GOAWAY HTTP3 frame
*/
public final class GoAwayFrame extends AbstractHttp3Frame {
public static final int TYPE = Http3FrameType.TYPE.GOAWAY_FRAME;
private final long length;
// represents either a stream id or a push id depending on the context
// of the frame
private final long id;
public GoAwayFrame(final long id) {
super(TYPE);
this.id = id;
// the payload length of this frame
this.length = VariableLengthEncoder.getEncodedSize(this.id);
}
// only used when constructing the frame during decoding content over a stream
private GoAwayFrame(final long length, final long id) {
super(Http3FrameType.GOAWAY.type());
this.length = length;
this.id = id;
}
@Override
public long length() {
return this.length;
}
/**
* {@return the id of either the stream or a push promise, depending on the context
* of this frame}
*/
public long getTargetId() {
return this.id;
}
public void writeFrame(final ByteBuffer buf) {
// write the type of the frame
VariableLengthEncoder.encode(buf, this.type);
// write the length of the payload
VariableLengthEncoder.encode(buf, this.length);
// write the stream id/push id
VariableLengthEncoder.encode(buf, this.id);
}
static AbstractHttp3Frame decodeFrame(final BuffersReader reader, final Logger debug) {
final long position = reader.position();
// read the frame type
decodeRequiredType(reader, Http3FrameType.GOAWAY.type());
// read length of the payload
final long length = VariableLengthEncoder.decode(reader);
if (length < 0 || length > reader.remaining()) {
reader.position(position);
throw new BufferUnderflowException();
}
// position before reading payload
long start = reader.position();
if (length == 0 || length != VariableLengthEncoder.peekEncodedValueSize(reader, start)) {
// frame length does not match the enclosed targetId
return new MalformedFrame(TYPE,
Http3Error.H3_FRAME_ERROR.code(),
"Invalid length in GOAWAY frame: " + length);
}
// read stream id / push id
final long targetId = VariableLengthEncoder.decode(reader);
if (targetId == -1) {
reader.position(position);
throw new BufferUnderflowException();
}
// check position after reading payload
var malformed = checkPayloadSize(TYPE, reader, start, length);
if (malformed != null) return malformed;
reader.release();
return new GoAwayFrame(length, targetId);
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder();
sb.append(super.toString()).append(" stream/push id: ").append(this.id);
return sb.toString();
}
}

View File

@ -0,0 +1,60 @@
/*
* 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.http3.frames;
/**
* This class models an HTTP/3 HEADERS frame.
* @apiNote
* An instance of {@code HeadersFrame} is used to read or writes
* the frame's type and length. The payload is supposed to be
* read or written directly to the stream on its own, after having
* read or written the frame type and length.
* @see jdk.internal.net.http.http3.frames.PartialFrame
*/
public final class HeadersFrame extends PartialFrame {
/**
* The HEADERS frame type, as defined by HTTP/3
*/
public static final int TYPE = Http3FrameType.TYPE.HEADERS_FRAME;
private final long length;
/**
* Creates a new HTTP/3 HEADERS frame
*/
public HeadersFrame(long length) {
super(TYPE, length);
this.length = length;
}
@Override
public long length() {
return length;
}
}

View File

@ -0,0 +1,214 @@
/*
* 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.http3.frames;
import java.util.function.LongPredicate;
import jdk.internal.net.http.common.Logger;
import jdk.internal.net.http.http3.Http3Error;
import jdk.internal.net.http.quic.BuffersReader;
import jdk.internal.net.http.quic.VariableLengthEncoder;
import static jdk.internal.net.http.http3.frames.Http3FrameType.DATA;
import static jdk.internal.net.http.http3.frames.Http3FrameType.HEADERS;
import static jdk.internal.net.http.http3.frames.Http3FrameType.PUSH_PROMISE;
import static jdk.internal.net.http.http3.frames.Http3FrameType.UNKNOWN;
import static jdk.internal.net.http.http3.frames.Http3FrameType.asString;
import static jdk.internal.net.http.http3.frames.Http3FrameType.isIllegalType;
/**
* An HTTP/3 frame
*/
public sealed interface Http3Frame permits AbstractHttp3Frame {
/**
* {@return the type of this frame}
*/
long type();
/**
* {@return the length of this frame}
*/
long length();
/**
* {@return the portion of the frame payload that can be read
* after the frame was created, when the current frame
* can be read as a partial frame, otherwise 0, if
* the payload can't be streamed}
*/
default long streamingLength() { return 0;}
/**
* Attempts to decode an HTTP/3 frame from the bytes accumulated
* in the reader.
*
* @apiNote
*
* If an error is detected while parsing the frame, a {@link MalformedFrame}
* error will be returned
*
* @param reader the reader containing the bytes
* @param isFrameTypeAllowed a predicate to test whether a given
* frame type is allowed in this context
* @param debug a logger to log debug traces
* @return the decoded frame, or {@code null} if some bytes are
* missing to decode the frame
*/
static Http3Frame decode(BuffersReader reader, LongPredicate isFrameTypeAllowed, Logger debug) {
long pos = reader.position();
long limit = reader.limit();
long remaining = reader.remaining();
long type = -1;
long before = reader.read();
Http3Frame frame;
try {
int tsize = VariableLengthEncoder.peekEncodedValueSize(reader, pos);
if (tsize == -1 || remaining - tsize < 0) return null;
type = VariableLengthEncoder.peekEncodedValue(reader, pos);
if (type == -1) return null;
if (isIllegalType(type) || !isFrameTypeAllowed.test(type)) {
var msg = "H3_FRAME_UNEXPECTED: Frame "
+ asString(type)
+ " is not allowed on this stream";
if (debug.on()) debug.log(msg);
frame = new MalformedFrame(type, Http3Error.H3_FRAME_UNEXPECTED.code(), msg);
reader.clear();
return frame;
}
int lsize = VariableLengthEncoder.peekEncodedValueSize(reader, pos + tsize);
if (lsize == -1 || remaining - tsize - lsize < 0) return null;
final long length = VariableLengthEncoder.peekEncodedValue(reader, pos + tsize);
var frameType = Http3FrameType.forType(type);
if (debug.on()) {
debug.log("Decoding %s(length=%s)", frameType, length);
}
if (frameType == UNKNOWN) {
if (debug.on()) {
debug.log("decode partial unknown frame: "
+ "pos:%s, limit:%s, remaining:%s," +
" tsize:%s, lsize:%s, length:%s",
pos, limit, remaining, tsize, lsize, length);
}
reader.position(pos + tsize + lsize);
reader.release();
return new UnknownFrame(type, length);
} else if (frameType.maxLength() < length) {
var msg = "H3_FRAME_ERROR: Frame " + asString(type) + " length too long";
if (debug.on()) debug.log(msg);
frame = new MalformedFrame(type, Http3Error.H3_FRAME_ERROR.code(), msg);
reader.clear();
return frame;
}
if (frameType == HEADERS) {
if (length == 0) {
var msg = "H3_FRAME_ERROR: Frame " + asString(type) + " does not contain headers";
if (debug.on()) debug.log(msg);
frame = new MalformedFrame(type, Http3Error.H3_FRAME_ERROR.code(), msg);
reader.clear();
return frame;
}
reader.position(pos + tsize + lsize);
reader.release();
return new HeadersFrame(length);
}
if (frameType == DATA) {
reader.position(pos + tsize + lsize);
reader.release();
return new DataFrame(length);
}
if (frameType == PUSH_PROMISE) {
int pidsize = VariableLengthEncoder.peekEncodedValueSize(reader, pos + tsize + lsize);
if (length == 0 || length < pidsize) {
var msg = "H3_FRAME_ERROR: Frame " + asString(type) + " length too short to fit pushID";
if (debug.on()) debug.log(msg);
frame = new MalformedFrame(type, Http3Error.H3_FRAME_ERROR.code(), msg);
reader.clear();
return frame;
}
if (length == pidsize) {
var msg = "H3_FRAME_ERROR: Frame " + asString(type) + " does not contain headers";
if (debug.on()) debug.log(msg);
frame = new MalformedFrame(type, Http3Error.H3_FRAME_ERROR.code(), msg);
reader.clear();
return frame;
}
if (pidsize == -1 || remaining - tsize - lsize - pidsize < 0) return null;
long pushId = VariableLengthEncoder.peekEncodedValue(reader, pos + tsize + lsize);
reader.position(pos + tsize + lsize + pidsize);
reader.release();
return new PushPromiseFrame(pushId, length - pidsize);
}
if (length + tsize + lsize > reader.remaining()) {
// we haven't moved the reader's position.
// we'll be called back when new bytes are available and
// we'll resume reading type + length from the same position
// again, until we have enough to read the frame.
return null;
}
assert isFrameTypeAllowed.test(type);
frame = switch(frameType) {
case SETTINGS -> SettingsFrame.decodeFrame(reader, debug);
case GOAWAY -> GoAwayFrame.decodeFrame(reader, debug);
case CANCEL_PUSH -> CancelPushFrame.decodeFrame(reader, debug);
case MAX_PUSH_ID -> MaxPushIdFrame.decodeFrame(reader, debug);
default -> {
reader.position(pos + tsize + lsize);
reader.release();
yield new UnknownFrame(type, length);
}
};
long read;
if (frame instanceof MalformedFrame || frame == null) {
return frame;
} else if ((read = (reader.read() - before - tsize - lsize)) != length) {
String msg = ("H3_FRAME_ERROR: Frame %s payload length does not match" +
" frame length (length=%s, payload=%s)")
.formatted(asString(type), length, read);
if (debug.on()) debug.log(msg);
reader.release(); // mark reader read
reader.position(reader.position() + tsize + lsize + length);
reader.release();
return new MalformedFrame(type, Http3Error.H3_FRAME_ERROR.code(), msg);
} else {
return frame;
}
} catch (Throwable t) {
if (debug.on()) debug.log("Failed to decode frame", t);
reader.clear(); // mark reader read
return new MalformedFrame(type, Http3Error.H3_INTERNAL_ERROR.code(), t.getMessage(), t);
}
}
}

View File

@ -0,0 +1,201 @@
/*
* 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.http3.frames;
import java.util.stream.Stream;
import static jdk.internal.net.http.quic.VariableLengthEncoder.MAX_ENCODED_INTEGER;
import static jdk.internal.net.http.quic.VariableLengthEncoder.MAX_INTEGER_LENGTH;
/**
* An enum to model HTTP/3 frame types.
*/
public enum Http3FrameType {
/**
* Used to identify an HTTP/3 frame whose type is unknown
*/
UNKNOWN(-1, MAX_ENCODED_INTEGER),
/**
* Used to identify an HTTP/3 DATA frame
*/
DATA(TYPE.DATA_FRAME, MAX_ENCODED_INTEGER),
/**
* Used to identify an HTTP/3 HEADERS frame
*/
HEADERS(TYPE.HEADERS_FRAME, MAX_ENCODED_INTEGER),
/**
* Used to identify an HTTP/3 CANCEL_PUSH frame
*/
CANCEL_PUSH(TYPE.CANCEL_PUSH_FRAME, MAX_INTEGER_LENGTH),
/**
* Used to identify an HTTP/3 SETTINGS frame
*/
SETTINGS(TYPE.SETTINGS_FRAME, TYPE.MAX_SETTINGS_LENGTH),
/**
* Used to identify an HTTP/3 PUSH_PROMISE frame
*/
PUSH_PROMISE(TYPE.PUSH_PROMISE_FRAME, MAX_ENCODED_INTEGER),
/**
* Used to identify an HTTP/3 GOAWAY frame
*/
GOAWAY(TYPE.GOAWAY_FRAME, MAX_INTEGER_LENGTH),
/**
* Used to identify an HTTP/3 MAX_PUSH_ID_FRAME frame
*/
MAX_PUSH_ID(TYPE.MAX_PUSH_ID_FRAME, MAX_INTEGER_LENGTH);
/**
* A class to hold type constants
*/
static final class TYPE {
private TYPE() { throw new InternalError(); }
// Frames types
public static final int DATA_FRAME = 0x00;
public static final int HEADERS_FRAME = 0x01;
public static final int CANCEL_PUSH_FRAME = 0x03;
public static final int SETTINGS_FRAME = 0x04;
public static final int PUSH_PROMISE_FRAME = 0x05;
public static final int GOAWAY_FRAME = 0x07;
public static final int MAX_PUSH_ID_FRAME = 0x0d;
// The maximum size a settings frame can have.
// This is a limit imposed by our implementation.
// There are only 7 settings defined in the current
// specification, but we will allow for a frame to
// contain up to 80. Past that limit, we will consider
// the frame to be malformed:
// 8 x 10 x (max sizeof(id) + max sizeof(value)) = 80 x 16 bytes
public static final long MAX_SETTINGS_LENGTH =
10L * 8L * MAX_INTEGER_LENGTH * 2L;
}
// This is one of the values defined in TYPE above, or
// -1 for the UNKNOWN frame types.
private final int type;
private final long maxLength;
private Http3FrameType(int type, long maxLength) {
this.type = type;
this.maxLength = maxLength;
}
/**
* {@return the frame type, as defined by HTTP/3}
*/
public long type() { return type;}
/**
* {@return the maximum length a frame of this type
* can take}
*/
public long maxLength() {
return maxLength;
}
/**
* {@return the HTTP/3 frame type, as an int}
*
* @apiNote
* HTTP/3 defines frames type as variable length integers
* in the range [0, 2^62-1]. However, the few standard frame
* types registered for HTTP/3 and modeled by this enum
* class can be coded as an int.
* This method provides a convenient way to access the frame
* type as an int, which avoids having to cast when using
* the value in switch statements.
*/
public int intType() { return type;}
/**
* {@return the {@link Http3FrameType} corresponding to the given
* {@code type}, or {@link #UNKNOWN} if no corresponding
* {@link Http3FrameType} instance is found}
* @param type an HTTP/3 frame type identifier read from an HTTP/3 frame
*/
public static Http3FrameType forType(long type) {
return Stream.of(values())
.filter(x -> x.type == type)
.findFirst()
.orElse(UNKNOWN);
}
/**
* {@return a string representation of the given type, suited for inclusion
* in log messages, exceptions, etc...}
* @param type an HTTP/3 frame type identifier read from an HTTP/3 frame
*/
public static String asString(long type) {
String str = null;
if (type >= Integer.MIN_VALUE && type <= Integer.MAX_VALUE) {
str = switch ((int)type) {
case TYPE.DATA_FRAME -> DATA.name(); // 0x00
case TYPE.HEADERS_FRAME -> HEADERS.name(); // 0x01
case 0x02 -> "RESERVED(0x02)";
case TYPE.CANCEL_PUSH_FRAME -> CANCEL_PUSH.name(); // 0x03
case TYPE.SETTINGS_FRAME -> SETTINGS.name(); // 0x04
case TYPE.PUSH_PROMISE_FRAME -> PUSH_PROMISE.name(); // 0x05
case 0x06 -> "RESERVED(0x06)";
case TYPE.GOAWAY_FRAME -> GOAWAY.name(); // 0x07
case 0x08 -> "RESERVED(0x08)";
case 0x09 -> "RESERVED(0x09)";
case TYPE.MAX_PUSH_ID_FRAME -> MAX_PUSH_ID.name(); // 0x0d
default -> null;
};
}
if (str != null) return str;
if (isReservedType(type)) {
return "RESERVED(type=" + type + ")";
}
return "UNKNOWN(type=" + type + ")";
}
/**
* {@return whether this frame type is illegal}
* This corresponds to HTTP/2 frame types that have no equivalent in
* HTTP/3.
* @param type the frame type
*/
public static boolean isIllegalType(long type) {
return type == 0x02 || type == 0x06 || type == 0x08 || type == 0x09;
}
/**
* Whether the given type is one of the reserved frame
* types defined by HTTP/3. For any non-negative integer N:
* {@code 0x21 + 0x1f * N }
* is a reserved frame type that has no meaning.
*
* @param type an HTTP/3 frame type identifier read from an HTTP/3 frame
*
* @return true if the given type matches the {@code 0x21 + 0x1f * N}
* pattern
*/
public static boolean isReservedType(long type) {
return type >= 0x21L && type <= MAX_ENCODED_INTEGER
&& (type - 0x21L) % 0x1f == 0;
}
}

View File

@ -0,0 +1,124 @@
/*
* 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.http3.frames;
import java.util.function.LongPredicate;
import jdk.internal.net.http.common.Logger;
import jdk.internal.net.http.quic.BuffersReader;
import jdk.internal.net.http.http3.Http3Error;
/**
* An instance of MalformedFrame can be returned by
* {@link AbstractHttp3Frame#decode(BuffersReader, LongPredicate, Logger)}
* when a malformed frame is detected. This should cause the caller
* to send an error to its peer, and possibly throw an
* exception to the higher layer.
*/
public class MalformedFrame extends AbstractHttp3Frame {
private final long errorCode;
private final String msg;
private final Throwable cause;
/**
* Creates Connection Error malformed frame
*
* @param errorCode - error code
* @param msg - internal debug message
*/
public MalformedFrame(long type, long errorCode, String msg) {
this(type, errorCode, msg, null);
}
/**
* Creates Connection Error malformed frame
*
* @param errorCode - error code
* @param msg - internal debug message
* @param cause - internal cause for the error, if available
* (can be null)
*/
public MalformedFrame(long type, long errorCode, String msg, Throwable cause) {
super(type);
this.errorCode = errorCode;
this.msg = msg;
this.cause = cause;
}
@Override
public String toString() {
return super.toString() + " MalformedFrame, Error: "
+ Http3Error.stringForCode(errorCode)
+ " reason: " + msg;
}
/**
* {@inheritDoc}
* @implSpec this method always returns 0
*/
@Override
public long length() {
return 0; // Not Applicable
}
/**
* {@inheritDoc}
* @implSpec this method always returns 0
*/
@Override
public long size() {
return 0; // Not applicable
}
/**
* {@return the {@linkplain Http3Error#code() HTTP/3 error code} that
* should be reported to the peer}
*/
public long getErrorCode() {
return errorCode;
}
/**
* {@return a message that describe the error}
*/
public String getMessage() {
return msg;
}
/**
* {@return the cause of the error, if available, {@code null} otherwise}
*
* @apiNote
* This is useful for logging and diagnosis purpose, typically when the
* error is an {@linkplain Http3Error#H3_INTERNAL_ERROR internal error}.
*/
public Throwable getCause() {
return cause;
}
}

Some files were not shown because too many files have changed in this diff Show More