mirror of
https://github.com/openjdk/jdk.git
synced 2026-01-28 12:09:14 +00:00
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:
parent
433d2ec534
commit
e8db14f584
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -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) + "]";
|
||||
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 = "";
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
699
src/java.base/share/classes/sun/security/ssl/QuicCipher.java
Normal file
699
src/java.base/share/classes/sun/security/ssl/QuicCipher.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
1216
src/java.base/share/classes/sun/security/ssl/QuicKeyManager.java
Normal file
1216
src/java.base/share/classes/sun/security/ssl/QuicKeyManager.java
Normal file
File diff suppressed because it is too large
Load Diff
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
*
|
||||
|
||||
@ -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 + "'");
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
#
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
176
src/java.net.http/share/classes/java/net/http/HttpOption.java
Normal file
176
src/java.net.http/share/classes/java/net/http/HttpOption.java
Normal 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
|
||||
}
|
||||
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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[]
|
||||
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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());
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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
@ -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
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -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();
|
||||
|
||||
/**
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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; }
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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()) {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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";
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 {
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user