Authors: russell@unturf.com · brackishbert@gmail.com · foxhop.net · TimeHexOn.com Patches, unit tests, benchmarks, whitepaper, and outreach briefs. Public domain — no copyright claimed. Use freely.
2125 lines
87 KiB
Java
2125 lines
87 KiB
Java
/*
|
|
* 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
|
|
* 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.Closeable;
|
|
import java.io.EOFException;
|
|
import java.io.IOException;
|
|
import java.io.UncheckedIOException;
|
|
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.channels.ClosedChannelException;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.time.Duration;
|
|
import java.util.ArrayList;
|
|
import java.util.Iterator;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import java.util.Map;
|
|
import java.util.Objects;
|
|
import java.util.concurrent.CompletableFuture;
|
|
import java.util.concurrent.ConcurrentHashMap;
|
|
import java.util.concurrent.ConcurrentLinkedQueue;
|
|
import java.util.concurrent.Flow;
|
|
import java.util.concurrent.atomic.AtomicBoolean;
|
|
import java.util.concurrent.atomic.AtomicInteger;
|
|
import java.util.concurrent.atomic.AtomicLong;
|
|
import java.util.concurrent.atomic.AtomicReference;
|
|
import java.util.concurrent.locks.Lock;
|
|
import java.util.concurrent.locks.ReentrantLock;
|
|
import java.util.function.Function;
|
|
import java.util.function.Supplier;
|
|
|
|
import javax.net.ssl.SSLEngine;
|
|
import javax.net.ssl.SSLException;
|
|
|
|
import jdk.internal.net.http.HttpConnection.HttpPublisher;
|
|
import jdk.internal.net.http.common.Alpns;
|
|
import jdk.internal.net.http.common.FlowTube;
|
|
import jdk.internal.net.http.common.FlowTube.TubeSubscriber;
|
|
import jdk.internal.net.http.common.HeaderDecoder;
|
|
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.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;
|
|
import jdk.internal.net.http.frame.FramesDecoder;
|
|
import jdk.internal.net.http.frame.FramesEncoder;
|
|
import jdk.internal.net.http.frame.GoAwayFrame;
|
|
import jdk.internal.net.http.frame.HeaderFrame;
|
|
import jdk.internal.net.http.frame.HeadersFrame;
|
|
import jdk.internal.net.http.frame.Http2Frame;
|
|
import jdk.internal.net.http.frame.MalformedFrame;
|
|
import jdk.internal.net.http.frame.OutgoingHeaders;
|
|
import jdk.internal.net.http.frame.PingFrame;
|
|
import jdk.internal.net.http.frame.PushPromiseFrame;
|
|
import jdk.internal.net.http.frame.ResetFrame;
|
|
import jdk.internal.net.http.frame.SettingsFrame;
|
|
import jdk.internal.net.http.frame.WindowUpdateFrame;
|
|
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;
|
|
import static jdk.internal.net.http.frame.SettingsFrame.INITIAL_WINDOW_SIZE;
|
|
import static jdk.internal.net.http.frame.SettingsFrame.MAX_CONCURRENT_STREAMS;
|
|
import static jdk.internal.net.http.frame.SettingsFrame.MAX_FRAME_SIZE;
|
|
import static jdk.internal.net.http.frame.SettingsFrame.MAX_HEADER_LIST_SIZE;
|
|
|
|
/**
|
|
* An Http2Connection. Encapsulates the socket(channel) and any SSLEngine used
|
|
* over it. Contains an HttpConnection which hides the SocketChannel SSL stuff.
|
|
*
|
|
* Http2Connections belong to a Http2ClientImpl, (one of) which belongs
|
|
* to a HttpClientImpl.
|
|
*
|
|
* Creation cases:
|
|
* 1) upgraded HTTP/1.1 plain tcp connection
|
|
* 2) prior knowledge directly created plain tcp connection
|
|
* 3) directly created HTTP/2 SSL connection which uses ALPN.
|
|
*
|
|
* Sending is done by writing directly to underlying HttpConnection object which
|
|
* is operating in async mode. No flow control applies on output at this level
|
|
* and all writes are just executed as puts to an output Q belonging to HttpConnection
|
|
* Flow control is implemented by HTTP/2 protocol itself.
|
|
*
|
|
* Hpack header compression
|
|
* and outgoing stream creation is also done here, because these operations
|
|
* must be synchronized at the socket level. Stream objects send frames simply
|
|
* by placing them on the connection's output Queue. sendFrame() is called
|
|
* from a higher level (Stream) thread.
|
|
*
|
|
* asyncReceive(ByteBuffer) is always called from the selector thread. It assembles
|
|
* incoming Http2Frames, and directs them to the appropriate Stream.incoming()
|
|
* or handles them directly itself. This thread performs hpack decompression
|
|
* and incoming stream creation (Server push). Incoming frames destined for a
|
|
* stream are provided by calling Stream.incoming().
|
|
*/
|
|
class Http2Connection implements Closeable {
|
|
|
|
final Logger debug = Utils.getDebugLogger(this::dbgString);
|
|
static final Logger DEBUG_LOGGER =
|
|
Utils.getDebugLogger("Http2Connection"::toString);
|
|
private final Logger debugHpack =
|
|
Utils.getHpackLogger(this::dbgString);
|
|
static final ByteBuffer EMPTY_TRIGGER = ByteBuffer.allocate(0);
|
|
|
|
private static final int MAX_CLIENT_STREAM_ID = Integer.MAX_VALUE; // 2147483647
|
|
private static final int MAX_SERVER_STREAM_ID = Integer.MAX_VALUE - 1; // 2147483646
|
|
// may be null; must be accessed/updated with the stateLock held
|
|
private IdleConnectionTimeoutEvent idleConnectionTimeoutEvent;
|
|
private final AtomicBoolean goAwaySent = new AtomicBoolean();
|
|
private final AtomicBoolean goAwayRecvd = new AtomicBoolean();
|
|
|
|
/**
|
|
* Flag set when no more streams to be opened on this connection.
|
|
* Two cases where it is used.
|
|
*
|
|
* 1. Two connections to the same server were opened concurrently, in which
|
|
* case one of them will be put in the cache, and the second will expire
|
|
* when all its opened streams (which usually should be a single client
|
|
* stream + possibly some additional push-promise server streams) complete.
|
|
* 2. A cached connection reaches its maximum number of streams (~ 2^31-1)
|
|
* either server / or client allocated, in which case it will be taken
|
|
* out of the cache - allowing a new connection to replace it. It will
|
|
* expire when all its still open streams (which could be many) eventually
|
|
* complete.
|
|
*/
|
|
private volatile boolean finalStream;
|
|
|
|
/*
|
|
* ByteBuffer pooling strategy for HTTP/2 protocol.
|
|
*
|
|
* In general there are 4 points where ByteBuffers are used:
|
|
* - incoming/outgoing frames from/to ByteBuffers plus incoming/outgoing
|
|
* encrypted data in case of SSL connection.
|
|
*
|
|
* 1. Outgoing frames encoded to ByteBuffers.
|
|
*
|
|
* Outgoing ByteBuffers are created with required size and frequently
|
|
* small (except DataFrames, etc). At this place no pools at all. All
|
|
* outgoing buffers should eventually be collected by GC.
|
|
*
|
|
* 2. Incoming ByteBuffers (decoded to frames).
|
|
*
|
|
* Here, total elimination of BB pool is not a good idea.
|
|
* We don't know how many bytes we will receive through network.
|
|
*
|
|
* A possible future improvement ( currently not implemented ):
|
|
* Allocate buffers of reasonable size. The following life of the BB:
|
|
* - If all frames decoded from the BB are other than DataFrame and
|
|
* HeaderFrame (and HeaderFrame subclasses) BB is returned to pool,
|
|
* - If a DataFrame is decoded from the BB. In that case DataFrame refers
|
|
* to sub-buffer obtained by slice(). Such a BB is never returned to the
|
|
* pool and will eventually be GC'ed.
|
|
* - If a HeadersFrame is decoded from the BB. Then header decoding is
|
|
* performed inside processFrame method and the buffer could be release
|
|
* back to pool.
|
|
*
|
|
* 3. SSL encrypted buffers ( received ).
|
|
*
|
|
* The current implementation recycles encrypted buffers read from the
|
|
* channel. The pool of buffers has a maximum size of 3, SocketTube.MAX_BUFFERS,
|
|
* direct buffers which are shared by all connections on a given client.
|
|
* The pool is used by all SSL connections - whether HTTP/1.1 or HTTP/2,
|
|
* but only for SSL encrypted buffers that circulate between the SocketTube
|
|
* Publisher and the SSLFlowDelegate Reader. Limiting the pool to this
|
|
* particular segment allows the use of direct buffers, thus avoiding any
|
|
* additional copy in the NIO socket channel implementation. See
|
|
* HttpClientImpl.SSLDirectBufferSupplier, SocketTube.SSLDirectBufferSource,
|
|
* and SSLTube.recycler.
|
|
*/
|
|
|
|
// An Idle connection is one that has no active streams
|
|
// and has not sent the final stream flag
|
|
final class IdleConnectionTimeoutEvent extends TimeoutEvent {
|
|
|
|
// expected to be accessed/updated with "stateLock" being held
|
|
private boolean cancelled;
|
|
|
|
IdleConnectionTimeoutEvent(Duration duration) {
|
|
super(duration);
|
|
}
|
|
|
|
/**
|
|
* {@link #close(Http2TerminationCause) Closes} the connection, unless this event is
|
|
* {@link #cancelled}
|
|
*/
|
|
@Override
|
|
public void handle() {
|
|
// first check if the connection is still idle.
|
|
// must be done with the "stateLock" held, to allow for synchronizing actions like
|
|
// closing the connection and checking out from connection pool (which too is expected
|
|
// to use this same lock)
|
|
stateLock.lock();
|
|
try {
|
|
if (cancelled) {
|
|
if (debug.on()) {
|
|
debug.log("Idle timeout event already cancelled, not initiating idle connection close");
|
|
}
|
|
return;
|
|
}
|
|
// the connection has been idle long enough, we now
|
|
// mark a state indicating that the connection is chosen
|
|
// for idle termination and should not be handed out (from the pool)
|
|
// for newer requests.
|
|
connTerminator.markForIdleTermination();
|
|
} finally {
|
|
stateLock.unlock();
|
|
}
|
|
// terminate the connection due to being idle long enough
|
|
connTerminator.idleTimedOut();
|
|
}
|
|
|
|
/**
|
|
* Cancels this event. Should be called with stateLock held
|
|
*/
|
|
void cancel() {
|
|
assert stateLock.isHeldByCurrentThread() : "Current thread doesn't hold " + stateLock;
|
|
// mark as cancelled to prevent potentially already triggered event from actually
|
|
// doing the close
|
|
this.cancelled = true;
|
|
// cancel the timer to prevent the event from being triggered (if it hasn't already)
|
|
client().cancelTimer(this);
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return "IdleConnectionTimeoutEvent, " + super.toString();
|
|
}
|
|
}
|
|
|
|
// A small class that allows to control frames with respect to the state of
|
|
// the connection preface. Any data received before the connection
|
|
// preface is sent will be buffered.
|
|
private final class FramesController {
|
|
volatile boolean prefaceSent;
|
|
volatile List<ByteBuffer> pending;
|
|
|
|
boolean processReceivedData(FramesDecoder decoder, ByteBuffer buf)
|
|
throws IOException
|
|
{
|
|
// if preface is not sent, buffers data in the pending list
|
|
if (!prefaceSent) {
|
|
if (debug.on())
|
|
debug.log("Preface not sent: buffering %d", buf.remaining());
|
|
stateLock.lock();
|
|
try {
|
|
if (!prefaceSent) {
|
|
if (pending == null) pending = new ArrayList<>();
|
|
pending.add(buf);
|
|
if (debug.on())
|
|
debug.log("there are now %d bytes buffered waiting for preface to be sent"
|
|
+ Utils.remaining(pending)
|
|
);
|
|
return false;
|
|
}
|
|
} finally {
|
|
stateLock.unlock();
|
|
}
|
|
}
|
|
|
|
// Preface is sent. Checks for pending data and flush it.
|
|
// We rely on this method being called from within the Http2TubeSubscriber
|
|
// scheduler, so we know that no other thread could execute this method
|
|
// concurrently while we're here.
|
|
// This ensures that later incoming buffers will not
|
|
// be processed before we have flushed the pending queue.
|
|
// No additional locking is therefore necessary here.
|
|
List<ByteBuffer> pending = this.pending;
|
|
this.pending = null;
|
|
if (pending != null) {
|
|
// flush pending data
|
|
if (debug.on()) debug.log(() -> "Processing buffered data: "
|
|
+ Utils.remaining(pending));
|
|
for (ByteBuffer b : pending) {
|
|
decoder.decode(b);
|
|
}
|
|
}
|
|
// push the received buffer to the frames decoder.
|
|
if (buf != EMPTY_TRIGGER) {
|
|
if (debug.on()) debug.log("Processing %d", buf.remaining());
|
|
decoder.decode(buf);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Mark that the connection preface is sent
|
|
void markPrefaceSent() {
|
|
assert !prefaceSent;
|
|
stateLock.lock();
|
|
try {
|
|
prefaceSent = true;
|
|
} finally {
|
|
stateLock.unlock();
|
|
}
|
|
}
|
|
}
|
|
|
|
private final class PushPromiseDecoder extends HeaderDecoder implements DecodingCallback {
|
|
|
|
final int parentStreamId;
|
|
final int pushPromiseStreamId;
|
|
final Stream<?> parent;
|
|
final AtomicReference<Throwable> errorRef = new AtomicReference<>();
|
|
|
|
PushPromiseDecoder(int parentStreamId, int pushPromiseStreamId, Stream<?> parent) {
|
|
super(Context.REQUEST);
|
|
this.parentStreamId = parentStreamId;
|
|
this.pushPromiseStreamId = pushPromiseStreamId;
|
|
this.parent = parent;
|
|
}
|
|
|
|
@Override
|
|
protected void addHeader(String name, String value) {
|
|
if (errorRef.get() == null) {
|
|
super.addHeader(name, value);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onMaxHeaderListSizeReached(long size, int maxHeaderListSize) throws ProtocolException {
|
|
try {
|
|
DecodingCallback.super.onMaxHeaderListSizeReached(size, maxHeaderListSize);
|
|
} catch (ProtocolException pe) {
|
|
if (parent != null) {
|
|
if (errorRef.compareAndSet(null, pe)) {
|
|
// cancel the parent stream
|
|
resetStream(pushPromiseStreamId, ResetFrame.REFUSED_STREAM);
|
|
parent.onProtocolError(pe);
|
|
}
|
|
} else {
|
|
// interrupt decoding and closes the connection
|
|
throw pe;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
private final ReentrantLock stateLock = new ReentrantLock();
|
|
final HttpConnection connection;
|
|
private final Http2ClientImpl client2;
|
|
private final ConcurrentHashMap<Integer,Stream<?>> streams = new ConcurrentHashMap<>();
|
|
private int nextstreamid;
|
|
private int nextPushStream = 2;
|
|
// actual stream ids are not allocated until the Headers frame is ready
|
|
// to be sent. The following two fields are updated as soon as a stream
|
|
// is created and assigned to a connection. They are checked before
|
|
// assigning a stream to a connection.
|
|
private int lastReservedClientStreamid = 1;
|
|
private int lastReservedServerStreamid = 0;
|
|
private int numReservedClientStreams = 0; // count of current streams
|
|
private int numReservedServerStreams = 0; // count of current streams
|
|
private final Encoder hpackOut;
|
|
private final Decoder hpackIn;
|
|
final SettingsFrame clientSettings;
|
|
private volatile SettingsFrame serverSettings;
|
|
|
|
private record PushContinuationState(PushPromiseDecoder pushContDecoder, PushPromiseFrame pushContFrame) {}
|
|
|
|
private volatile PushContinuationState pushContinuationState;
|
|
private final String key; // for HttpClientImpl.connections map
|
|
private final FramesDecoder framesDecoder;
|
|
private final FramesEncoder framesEncoder = new FramesEncoder();
|
|
private final AtomicLong lastProcessedStreamInGoAway = new AtomicLong(-1);
|
|
|
|
/**
|
|
* Send Window controller for both connection and stream windows.
|
|
* Each of this connection's Streams MUST use this controller.
|
|
*/
|
|
private final WindowController windowController = new WindowController();
|
|
private final FramesController framesController = new FramesController();
|
|
private final Http2TubeSubscriber subscriber;
|
|
final ConnectionWindowUpdateSender windowUpdater;
|
|
private final Terminator connTerminator = new Terminator();
|
|
private volatile Supplier<ByteBuffer> initial;
|
|
private volatile Stream<?> initialStream;
|
|
|
|
private ValidatingHeadersConsumer orphanedConsumer;
|
|
private final AtomicInteger orphanedHeaders = new AtomicInteger();
|
|
|
|
static final int DEFAULT_FRAME_SIZE = 16 * 1024;
|
|
static final int MAX_LITERAL_WITH_INDEXING =
|
|
Utils.getIntegerNetProperty("jdk.httpclient.maxLiteralWithIndexing",512);
|
|
|
|
// The maximum number of HEADER frames, CONTINUATION frames, or PUSH_PROMISE frames
|
|
// referring to an already closed or non-existent stream that a client will accept to
|
|
// process. Receiving frames referring to non-existent or closed streams doesn't necessarily
|
|
// constitute an HTTP/2 protocol error, but receiving too many may indicate a problem
|
|
// with the connection. If this limit is reached, a {@link java.net.ProtocolException
|
|
// ProtocolException} will be raised and the connection will be closed.
|
|
static final int MAX_ORPHANED_HEADERS = 1024;
|
|
|
|
// TODO: need list of control frames from other threads
|
|
// that need to be sent
|
|
|
|
private Http2Connection(HttpConnection connection,
|
|
Http2ClientImpl client2,
|
|
int nextstreamid,
|
|
String key,
|
|
boolean defaultServerPush) {
|
|
this.connection = connection;
|
|
this.client2 = client2;
|
|
this.subscriber = new Http2TubeSubscriber(client2.client());
|
|
this.nextstreamid = nextstreamid;
|
|
this.key = key;
|
|
this.clientSettings = this.client2.getClientSettings(defaultServerPush);
|
|
this.framesDecoder = new FramesDecoder(this::processFrame,
|
|
clientSettings.getParameter(SettingsFrame.MAX_FRAME_SIZE));
|
|
// serverSettings will be updated by server
|
|
this.serverSettings = SettingsFrame.defaultRFCSettings();
|
|
this.hpackOut = new Encoder(serverSettings.getParameter(HEADER_TABLE_SIZE));
|
|
this.hpackIn = new Decoder(clientSettings.getParameter(HEADER_TABLE_SIZE),
|
|
clientSettings.getParameter(MAX_HEADER_LIST_SIZE), MAX_LITERAL_WITH_INDEXING);
|
|
if (debugHpack.on()) {
|
|
debugHpack.log("For the record:" + super.toString());
|
|
debugHpack.log("Decoder created: %s", hpackIn);
|
|
debugHpack.log("Encoder created: %s", hpackOut);
|
|
}
|
|
this.windowUpdater = new ConnectionWindowUpdateSender(this,
|
|
client2.getConnectionWindowSize(clientSettings));
|
|
}
|
|
|
|
/**
|
|
* Case 1) Create from upgraded HTTP/1.1 connection.
|
|
* Is ready to use. Can't be SSL. exchange is the Exchange
|
|
* that initiated the connection, whose response will be delivered
|
|
* on a Stream.
|
|
*/
|
|
private Http2Connection(HttpConnection connection,
|
|
Http2ClientImpl client2,
|
|
Exchange<?> exchange,
|
|
Supplier<ByteBuffer> initial,
|
|
boolean defaultServerPush)
|
|
throws IOException, InterruptedException
|
|
{
|
|
this(connection,
|
|
client2,
|
|
3, // stream 1 is registered during the upgrade
|
|
keyFor(connection),
|
|
defaultServerPush);
|
|
reserveStream(true, clientSettings.getFlag(ENABLE_PUSH));
|
|
Log.logTrace("Connection send window size {0} ", windowController.connectionWindowSize());
|
|
|
|
Stream<?> initialStream = createStream(exchange);
|
|
boolean opened = initialStream.registerStream(1, true);
|
|
this.initialStream = initialStream;
|
|
if (debug.on() && !opened) {
|
|
debug.log("Initial stream was cancelled - but connection is maintained: " +
|
|
"reset frame will need to be sent later");
|
|
}
|
|
windowController.registerStream(1, getInitialSendWindowSize());
|
|
initialStream.requestSent();
|
|
// Upgrading:
|
|
// set callbacks before sending preface - makes sure anything that
|
|
// might be sent by the server will come our way.
|
|
this.initial = initial;
|
|
connectFlows(connection);
|
|
sendConnectionPreface();
|
|
if (!opened) {
|
|
debug.log("ensure reset frame is sent to cancel initial stream");
|
|
initialStream.sendResetStreamFrame(ResetFrame.CANCEL);
|
|
}
|
|
|
|
}
|
|
|
|
// Used when upgrading an HTTP/1.1 connection to HTTP/2 after receiving
|
|
// agreement from the server. Async style but completes immediately, because
|
|
// the connection is already connected.
|
|
static CompletableFuture<Http2Connection> createAsync(HttpConnection connection,
|
|
Http2ClientImpl client2,
|
|
Exchange<?> exchange,
|
|
Supplier<ByteBuffer> initial)
|
|
{
|
|
return MinimalFuture.supply(() -> new Http2Connection(connection, client2, exchange, initial,
|
|
exchange.pushEnabled()));
|
|
}
|
|
|
|
// Requires TLS handshake. So, is really async
|
|
static CompletableFuture<Http2Connection> createAsync(HttpRequestImpl request,
|
|
Http2ClientImpl h2client,
|
|
Exchange<?> exchange) {
|
|
assert request.secure();
|
|
AbstractAsyncSSLConnection connection = (AbstractAsyncSSLConnection)
|
|
HttpConnection.getConnection(request.getAddress(),
|
|
h2client.client(),
|
|
exchange,
|
|
request,
|
|
HttpClient.Version.HTTP_2);
|
|
|
|
// Expose the underlying connection to the exchange's aborter so it can
|
|
// be closed if a timeout occurs.
|
|
exchange.connectionAborter.connection(connection);
|
|
|
|
return connection.connectAsync(exchange)
|
|
.thenCompose(unused -> connection.finishConnect())
|
|
.thenCompose(unused -> checkSSLConfig(connection))
|
|
.thenCompose(notused-> {
|
|
CompletableFuture<Http2Connection> cf = new MinimalFuture<>();
|
|
try {
|
|
Http2Connection hc = new Http2Connection(request, h2client,
|
|
connection, exchange.pushEnabled());
|
|
cf.complete(hc);
|
|
} catch (IOException e) {
|
|
cf.completeExceptionally(e);
|
|
}
|
|
return cf; } );
|
|
}
|
|
|
|
/**
|
|
* Cases 2) 3)
|
|
*
|
|
* request is request to be sent.
|
|
*/
|
|
private Http2Connection(HttpRequestImpl request,
|
|
Http2ClientImpl h2client,
|
|
HttpConnection connection,
|
|
boolean defaultServerPush)
|
|
throws IOException
|
|
{
|
|
this(connection,
|
|
h2client,
|
|
1,
|
|
keyFor(request),
|
|
defaultServerPush);
|
|
|
|
Log.logTrace("Connection send window size {0} ", windowController.connectionWindowSize());
|
|
|
|
// safe to resume async reading now.
|
|
connectFlows(connection);
|
|
sendConnectionPreface();
|
|
}
|
|
|
|
private void connectFlows(HttpConnection connection) {
|
|
FlowTube tube = connection.getConnectionFlow();
|
|
// Connect the flow to our Http2TubeSubscriber:
|
|
tube.connectFlows(connection.publisher(), subscriber);
|
|
}
|
|
|
|
final HttpClientImpl client() {
|
|
return client2.client();
|
|
}
|
|
|
|
// call these before assigning a request/stream to a connection
|
|
// if false returned then a new Http2Connection is required
|
|
// if true, the stream may be assigned to this connection
|
|
// for server push, if false returned, then the stream should be cancelled
|
|
boolean reserveStream(boolean clientInitiated, boolean pushEnabled) throws IOException {
|
|
stateLock.lock();
|
|
try {
|
|
return reserveStream0(clientInitiated, pushEnabled);
|
|
} finally {
|
|
stateLock.unlock();
|
|
}
|
|
}
|
|
|
|
private boolean reserveStream0(boolean clientInitiated, boolean pushEnabled) throws IOException {
|
|
if (finalStream()) {
|
|
return false;
|
|
}
|
|
// If requesting to reserve a stream for an exchange for which push is enabled,
|
|
// we will reserve the stream in this connection only if this connection is also
|
|
// push enabled, unless pushes are globally disabled.
|
|
boolean pushCompatible = !clientInitiated || !pushEnabled
|
|
|| this.serverPushEnabled()
|
|
|| client2.serverPushDisabled();
|
|
if (clientInitiated && (lastReservedClientStreamid >= MAX_CLIENT_STREAM_ID -2 || !pushCompatible)) {
|
|
setFinalStream();
|
|
client2.removeFromPool(this);
|
|
return false;
|
|
} else if (!clientInitiated && (lastReservedServerStreamid >= MAX_SERVER_STREAM_ID - 2)) {
|
|
setFinalStream();
|
|
client2.removeFromPool(this);
|
|
return false;
|
|
}
|
|
if (clientInitiated)
|
|
lastReservedClientStreamid+=2;
|
|
else
|
|
lastReservedServerStreamid+=2;
|
|
|
|
assert numReservedClientStreams >= 0;
|
|
assert numReservedServerStreams >= 0;
|
|
if (clientInitiated &&numReservedClientStreams >= maxConcurrentClientInitiatedStreams()) {
|
|
throw new IOException("too many concurrent streams");
|
|
} else if (clientInitiated) {
|
|
numReservedClientStreams++;
|
|
}
|
|
if (!clientInitiated && numReservedServerStreams >= maxConcurrentServerInitiatedStreams()) {
|
|
return false;
|
|
} else if (!clientInitiated) {
|
|
numReservedServerStreams++;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void abandonStream() {
|
|
stateLock.lock();
|
|
try {
|
|
long reserved = --numReservedClientStreams;
|
|
assert reserved >= 0;
|
|
} catch (Throwable t) {
|
|
close(Http2TerminationCause.forException(t)); // in case the assert fires...
|
|
} finally {
|
|
stateLock.unlock();
|
|
}
|
|
|
|
// if the connection is eligible to be closed, we close it here
|
|
if (shouldClose()) {
|
|
close(Http2TerminationCause.noErrorTermination());
|
|
}
|
|
}
|
|
|
|
/*
|
|
* return true if the connection is marked as "final stream" and there
|
|
* are no active streams on that connection and the connection isn't
|
|
* reserved for a new stream.
|
|
*/
|
|
final boolean shouldClose() {
|
|
stateLock.lock();
|
|
try {
|
|
return finalStream() && streams.isEmpty() && numReservedClientStreams == 0;
|
|
} finally {
|
|
stateLock.unlock();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Throws an IOException if h2 was not negotiated
|
|
*/
|
|
private static CompletableFuture<?> checkSSLConfig(AbstractAsyncSSLConnection aconn) {
|
|
assert aconn.isSecure();
|
|
|
|
Function<String, CompletableFuture<Void>> checkAlpnCF = (alpn) -> {
|
|
CompletableFuture<Void> cf = new MinimalFuture<>();
|
|
SSLEngine engine = aconn.getEngine();
|
|
String engineAlpn = engine.getApplicationProtocol();
|
|
DEBUG_LOGGER.log("checkSSLConfig: alpn: '%s', engine: '%s'", alpn, engineAlpn);
|
|
if (alpn == null && engineAlpn != null) {
|
|
alpn = engineAlpn;
|
|
}
|
|
DEBUG_LOGGER.log("checkSSLConfig: alpn: '%s'", alpn );
|
|
|
|
if (alpn == null || !alpn.equals(Alpns.H2)) {
|
|
String msg;
|
|
if (alpn == null) {
|
|
Log.logSSL("ALPN not supported");
|
|
msg = "ALPN not supported";
|
|
} else {
|
|
switch (alpn) {
|
|
case "":
|
|
Log.logSSL(msg = "No ALPN negotiated");
|
|
break;
|
|
case Alpns.HTTP_1_1:
|
|
Log.logSSL( msg = "HTTP/1.1 ALPN returned");
|
|
break;
|
|
default:
|
|
Log.logSSL(msg = "Unexpected ALPN: " + alpn);
|
|
cf.completeExceptionally(new IOException(msg));
|
|
}
|
|
}
|
|
cf.completeExceptionally(new ALPNException(msg, aconn));
|
|
return cf;
|
|
}
|
|
assert Objects.equals(alpn, engineAlpn)
|
|
: "alpn: %s, engine: %s".formatted(alpn, engineAlpn);
|
|
cf.complete(null);
|
|
return cf;
|
|
};
|
|
|
|
return aconn.getALPN()
|
|
.whenComplete((r,t) -> {
|
|
if (t != null && t instanceof SSLException) {
|
|
// something went wrong during the initial handshake
|
|
// close the connection
|
|
aconn.close(t);
|
|
}
|
|
})
|
|
.thenCompose(checkAlpnCF);
|
|
}
|
|
|
|
boolean finalStream() {
|
|
return finalStream;
|
|
}
|
|
|
|
/**
|
|
* Mark this connection so no more streams created on it and it will close when
|
|
* all are complete.
|
|
*/
|
|
void setFinalStream() {
|
|
finalStream = true;
|
|
}
|
|
|
|
static String keyFor(HttpConnection connection) {
|
|
boolean isProxy = connection.isProxied(); // tunnel or plain clear connection through proxy
|
|
boolean isSecure = connection.isSecure();
|
|
InetSocketAddress addr = connection.address();
|
|
InetSocketAddress proxyAddr = connection.proxy();
|
|
assert isProxy == (proxyAddr != null);
|
|
|
|
return keyString(isSecure, proxyAddr, addr.getHostString(), addr.getPort());
|
|
}
|
|
|
|
static String keyFor(final HttpRequestImpl request) {
|
|
final InetSocketAddress targetAddr = request.getAddress();
|
|
final InetSocketAddress proxy = request.proxy();
|
|
final boolean secure = request.secure();
|
|
return keyString(secure, proxy, targetAddr.getHostString(), targetAddr.getPort());
|
|
}
|
|
|
|
// Compute the key for an HttpConnection in the Http2ClientImpl pool:
|
|
// The key string follows one of the three forms below:
|
|
// {C,S}:H:host:port
|
|
// C:P:proxy-host:proxy-port
|
|
// S:T:H:host:port;P:proxy-host:proxy-port
|
|
// C indicates clear text connection "http"
|
|
// S indicates secure "https"
|
|
// H indicates host (direct) connection
|
|
// P indicates proxy
|
|
// T indicates a tunnel connection through a proxy
|
|
//
|
|
// The first form indicates a direct connection to a server:
|
|
// - direct clear connection to an HTTP host:
|
|
// e.g.: "C:H:foo.com:80"
|
|
// - direct secure connection to an HTTPS host:
|
|
// e.g.: "S:H:foo.com:443"
|
|
// The second form indicates a clear connection to an HTTP/1.1 proxy:
|
|
// e.g.: "C:P:myproxy:8080"
|
|
// The third form indicates a secure tunnel connection to an HTTPS
|
|
// host through an HTTP/1.1 proxy:
|
|
// e.g: "S:T:H:foo.com:80;P:myproxy:8080"
|
|
static String keyString(boolean secure, InetSocketAddress proxy, String host, int port) {
|
|
if (secure && port == -1)
|
|
port = 443;
|
|
else if (!secure && port == -1)
|
|
port = 80;
|
|
var key = (secure ? "S:" : "C:");
|
|
if (proxy != null && !secure) {
|
|
// clear connection through proxy
|
|
key = key + "P:" + proxy.getHostString() + ":" + proxy.getPort();
|
|
} else if (proxy == null) {
|
|
// direct connection to host
|
|
key = key + "H:" + host + ":" + port;
|
|
} else {
|
|
// tunnel connection through proxy
|
|
key = key + "T:H:" + host + ":" + port + ";P:" + proxy.getHostString() + ":" + proxy.getPort();
|
|
}
|
|
return key;
|
|
}
|
|
|
|
String key() {
|
|
return this.key;
|
|
}
|
|
|
|
public boolean serverPushEnabled() {
|
|
return clientSettings.getParameter(SettingsFrame.ENABLE_PUSH) == 1;
|
|
}
|
|
|
|
boolean offerConnection() {
|
|
return client2.offerConnection(this);
|
|
}
|
|
|
|
private HttpPublisher publisher() {
|
|
return connection.publisher();
|
|
}
|
|
|
|
private void decodeHeaders(HeaderFrame frame, DecodingCallback decoder)
|
|
throws IOException
|
|
{
|
|
if (debugHpack.on()) debugHpack.log("decodeHeaders(%s)", decoder);
|
|
|
|
boolean endOfHeaders = frame.getFlag(HeaderFrame.END_HEADERS);
|
|
|
|
List<ByteBuffer> buffers = frame.getHeaderBlock();
|
|
int len = buffers.size();
|
|
for (int i = 0; i < len; i++) {
|
|
ByteBuffer b = buffers.get(i);
|
|
hpackIn.decode(b, endOfHeaders && (i == len - 1), decoder);
|
|
}
|
|
}
|
|
|
|
final int getInitialSendWindowSize() {
|
|
return serverSettings.getParameter(INITIAL_WINDOW_SIZE);
|
|
}
|
|
|
|
final int maxConcurrentClientInitiatedStreams() {
|
|
return serverSettings.getParameter(MAX_CONCURRENT_STREAMS);
|
|
}
|
|
|
|
final int maxConcurrentServerInitiatedStreams() {
|
|
return clientSettings.getParameter(MAX_CONCURRENT_STREAMS);
|
|
}
|
|
|
|
long count;
|
|
|
|
final void asyncReceive(ByteBuffer buffer) {
|
|
// We don't need to read anything and
|
|
// we don't want to send anything back to the server
|
|
// until the connection preface has been sent.
|
|
// Therefore we're going to wait if needed before reading
|
|
// (and thus replying) to anything.
|
|
// Starting to reply to something (e.g send an ACK to a
|
|
// SettingsFrame sent by the server) before the connection
|
|
// preface is fully sent might result in the server
|
|
// sending a GOAWAY frame with 'invalid_preface'.
|
|
//
|
|
// Note: asyncReceive is only called from the Http2TubeSubscriber
|
|
// sequential scheduler.
|
|
try {
|
|
Supplier<ByteBuffer> bs = initial;
|
|
// ensure that we always handle the initial buffer first,
|
|
// if any.
|
|
if (bs != null) {
|
|
initial = null;
|
|
ByteBuffer b = bs.get();
|
|
if (b.hasRemaining()) {
|
|
long c = ++count;
|
|
if (debug.on())
|
|
debug.log(() -> "H2 Receiving Initial(" + c +"): " + b.remaining());
|
|
framesController.processReceivedData(framesDecoder, b);
|
|
}
|
|
}
|
|
ByteBuffer b = buffer;
|
|
// the Http2TubeSubscriber scheduler ensures that the order of incoming
|
|
// buffers is preserved.
|
|
if (b == EMPTY_TRIGGER) {
|
|
if (debug.on()) debug.log("H2 Received EMPTY_TRIGGER");
|
|
boolean prefaceSent = framesController.prefaceSent;
|
|
assert prefaceSent;
|
|
// call framesController.processReceivedData to potentially
|
|
// trigger the processing of all the data buffered there.
|
|
framesController.processReceivedData(framesDecoder, buffer);
|
|
if (debug.on()) debug.log("H2 processed buffered data");
|
|
} else {
|
|
long c = ++count;
|
|
if (debug.on())
|
|
debug.log("H2 Receiving(%d): %d", c, b.remaining());
|
|
framesController.processReceivedData(framesDecoder, buffer);
|
|
if (debug.on()) debug.log("H2 processed(%d)", c);
|
|
}
|
|
} catch (Throwable e) {
|
|
String msg = Utils.stackTrace(e);
|
|
Log.logTrace(msg);
|
|
close(Http2TerminationCause.forException(e));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Closes the connection normally (with a NO_ERROR termination cause), if not already closed.
|
|
*/
|
|
@Override
|
|
public final void close() {
|
|
close(Http2TerminationCause.noErrorTermination());
|
|
}
|
|
|
|
/**
|
|
* Closes the connection with the given termination cause, if not already closed.
|
|
*
|
|
* @param tc the termination cause. cannot be null.
|
|
*/
|
|
final void close(final Http2TerminationCause tc) {
|
|
Objects.requireNonNull(tc, "termination cause cannot be null");
|
|
this.connTerminator.terminate(tc);
|
|
}
|
|
|
|
/**
|
|
* Streams initiated by a client MUST use odd-numbered stream
|
|
* identifiers; those initiated by the server MUST use even-numbered
|
|
* stream identifiers.
|
|
*/
|
|
private static final boolean isServerInitiatedStream(int streamid) {
|
|
return (streamid & 0x1) == 0;
|
|
}
|
|
|
|
/**
|
|
* Handles stream 0 (common) frames that apply to whole connection and passes
|
|
* other stream specific frames to that Stream object.
|
|
*
|
|
* Invokes Stream.incoming() which is expected to process frame without
|
|
* blocking.
|
|
*/
|
|
void processFrame(Http2Frame frame) throws IOException {
|
|
Log.logFrames(frame, "IN");
|
|
int streamid = frame.streamid();
|
|
if (frame instanceof MalformedFrame) {
|
|
Log.logError(((MalformedFrame) frame).getMessage());
|
|
if (streamid == 0) {
|
|
framesDecoder.close("Malformed frame on stream 0");
|
|
protocolError(((MalformedFrame) frame).getErrorCode(),
|
|
((MalformedFrame) frame).getMessage());
|
|
} else {
|
|
if (debug.on())
|
|
debug.log(() -> "Reset stream: " + ((MalformedFrame) frame).getMessage());
|
|
resetStream(streamid, ((MalformedFrame) frame).getErrorCode());
|
|
}
|
|
return;
|
|
}
|
|
if (streamid == 0) {
|
|
handleConnectionFrame(frame);
|
|
} else {
|
|
if (frame instanceof SettingsFrame) {
|
|
// The stream identifier for a SETTINGS frame MUST be zero
|
|
protocolError(ErrorFrame.PROTOCOL_ERROR,
|
|
"The stream identifier for a SETTINGS frame MUST be zero");
|
|
return;
|
|
}
|
|
|
|
if (frame instanceof PushPromiseFrame && !serverPushEnabled()) {
|
|
String protocolError = "received a PUSH_PROMISE when SETTINGS_ENABLE_PUSH is 0";
|
|
protocolError(ErrorFrame.PROTOCOL_ERROR, protocolError);
|
|
return;
|
|
}
|
|
|
|
Stream<?> stream = getStream(streamid);
|
|
var nextstreamid = this.nextstreamid;
|
|
if (stream == null && (streamid & 0x01) == 0x01 && streamid >= nextstreamid) {
|
|
String protocolError = String.format(
|
|
"received a frame for a non existing streamid(%s) >= nextstreamid(%s)",
|
|
streamid, nextstreamid);
|
|
protocolError(ResetFrame.PROTOCOL_ERROR, protocolError);
|
|
return;
|
|
}
|
|
PushContinuationState pcs = pushContinuationState;
|
|
if (stream == null && pcs == null) {
|
|
// Should never receive a frame with unknown stream id
|
|
|
|
if (frame instanceof HeaderFrame hf) {
|
|
String protocolError = checkMaxOrphanedHeadersExceeded(hf);
|
|
if (protocolError != null) {
|
|
protocolError(ResetFrame.PROTOCOL_ERROR, protocolError);
|
|
return;
|
|
}
|
|
// always decode the headers as they may affect
|
|
// connection-level HPACK decoding state
|
|
if (orphanedConsumer == null || frame.getClass() != ContinuationFrame.class) {
|
|
orphanedConsumer = new ValidatingHeadersConsumer(
|
|
frame instanceof PushPromiseFrame ?
|
|
Context.REQUEST :
|
|
Context.RESPONSE);
|
|
}
|
|
DecodingCallback decoder = orphanedConsumer::onDecoded;
|
|
try {
|
|
decodeHeaders(hf, decoder);
|
|
} catch (IOException | UncheckedIOException e) {
|
|
protocolError(ResetFrame.PROTOCOL_ERROR, e.getMessage());
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!(frame instanceof ResetFrame)) {
|
|
if (frame instanceof DataFrame df) {
|
|
dropDataFrame(df);
|
|
}
|
|
if (isServerInitiatedStream(streamid)) {
|
|
if (streamid < nextPushStream) {
|
|
// trailing data on a cancelled push promise stream,
|
|
// reset will already have been sent, ignore
|
|
Log.logTrace("Ignoring cancelled push promise frame " + frame);
|
|
} else {
|
|
resetStream(streamid, ResetFrame.PROTOCOL_ERROR);
|
|
}
|
|
} else if (streamid >= nextstreamid) {
|
|
// otherwise the stream has already been reset/closed
|
|
resetStream(streamid, ResetFrame.PROTOCOL_ERROR);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// While push frame is not null, the only acceptable frame on this
|
|
// stream is a Continuation frame
|
|
if (pcs != null) {
|
|
if (frame instanceof ContinuationFrame cf) {
|
|
if (stream == null) {
|
|
String protocolError = checkMaxOrphanedHeadersExceeded(cf);
|
|
if (protocolError != null) {
|
|
protocolError(ResetFrame.PROTOCOL_ERROR, protocolError);
|
|
return;
|
|
}
|
|
}
|
|
try {
|
|
if (streamid == pcs.pushContFrame.streamid())
|
|
handlePushContinuation(pcs, stream, cf);
|
|
else {
|
|
String protocolError = "Received a CONTINUATION with " +
|
|
"unexpected stream id: " + streamid + " != "
|
|
+ pcs.pushContFrame.streamid();
|
|
protocolError(ErrorFrame.PROTOCOL_ERROR, protocolError);
|
|
}
|
|
} catch (IOException | UncheckedIOException e) {
|
|
debug.log("Error handling Push Promise with Continuation: " + e.getMessage(), e);
|
|
protocolError(ErrorFrame.PROTOCOL_ERROR, e.getMessage());
|
|
return;
|
|
}
|
|
} else {
|
|
pushContinuationState = null;
|
|
String protocolError = "Expected a CONTINUATION frame but received " + frame;
|
|
protocolError(ErrorFrame.PROTOCOL_ERROR, protocolError);
|
|
return;
|
|
}
|
|
} else {
|
|
if (frame instanceof PushPromiseFrame pp) {
|
|
try {
|
|
handlePushPromise(stream, pp);
|
|
} catch (IOException | UncheckedIOException e) {
|
|
protocolError(ErrorFrame.PROTOCOL_ERROR, e.getMessage());
|
|
return;
|
|
}
|
|
} else if (frame instanceof HeaderFrame hf) {
|
|
// decode headers
|
|
try {
|
|
decodeHeaders(hf, stream.rspHeadersConsumer());
|
|
} catch (IOException | UncheckedIOException e) {
|
|
debug.log("Error decoding headers: " + e.getMessage(), e);
|
|
protocolError(ErrorFrame.PROTOCOL_ERROR, e.getMessage());
|
|
return;
|
|
}
|
|
stream.incoming(frame);
|
|
} else {
|
|
stream.incoming(frame);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private String checkMaxOrphanedHeadersExceeded(HeaderFrame hf) {
|
|
if (MAX_ORPHANED_HEADERS > 0 ) {
|
|
int orphaned = orphanedHeaders.incrementAndGet();
|
|
if (orphaned < 0 || orphaned > MAX_ORPHANED_HEADERS) {
|
|
return "Too many orphaned header frames received on connection";
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// This method is called when a DataFrame that was added
|
|
// to a Stream::inputQ is later dropped from the queue
|
|
// without being consumed.
|
|
//
|
|
// Before adding a frame to the queue, the Stream calls
|
|
// connection.windowUpdater.canBufferUnprocessedBytes(), which
|
|
// increases the count of unprocessed bytes in the connection.
|
|
// After consuming the frame, it calls connection.windowUpdater::processed,
|
|
// which decrements the count of unprocessed bytes, and possibly
|
|
// sends a window update to the peer.
|
|
//
|
|
// This method is called when connection.windowUpdater::processed
|
|
// will not be called, which can happen when consuming the frame
|
|
// fails, or when an empty DataFrame terminates the stream,
|
|
// or when the stream is cancelled while data is still
|
|
// sitting in its inputQ. In the later case, it is called for
|
|
// each frame that is dropped from the queue.
|
|
final void releaseUnconsumed(DataFrame df) {
|
|
windowUpdater.released(df.payloadLength());
|
|
dropDataFrame(df);
|
|
}
|
|
|
|
// This method can be called directly when a DataFrame is dropped
|
|
// before/without having been added to any Stream::inputQ.
|
|
// In that case, the number of unprocessed bytes hasn't been incremented
|
|
// by the stream, and does not need to be decremented.
|
|
// Otherwise, if the frame is dropped after having been added to the
|
|
// inputQ, releaseUnconsumed above should be called.
|
|
final void dropDataFrame(DataFrame df) {
|
|
if (!isOpen()) {
|
|
return;
|
|
}
|
|
if (debug.on()) {
|
|
debug.log("Dropping data frame for stream %d (%d payload bytes)",
|
|
df.streamid(), df.payloadLength());
|
|
}
|
|
ensureWindowUpdated(df);
|
|
}
|
|
|
|
final void ensureWindowUpdated(DataFrame df) {
|
|
try {
|
|
if (!isOpen()) {
|
|
return;
|
|
}
|
|
int length = df.payloadLength();
|
|
if (length > 0) {
|
|
windowUpdater.update(length);
|
|
}
|
|
} catch(Throwable t) {
|
|
Log.logError("Unexpected exception while updating window: {0}", (Object)t);
|
|
}
|
|
}
|
|
|
|
private <T> void handlePushPromise(Stream<T> parent, PushPromiseFrame pp)
|
|
throws IOException
|
|
{
|
|
int promisedStreamid = pp.getPromisedStream();
|
|
if ((promisedStreamid & 0x01) != 0x00) {
|
|
throw new ProtocolException("Received PUSH_PROMISE for stream " + promisedStreamid);
|
|
}
|
|
int streamId = pp.streamid();
|
|
if ((streamId & 0x01) != 0x01) {
|
|
throw new ProtocolException("Received PUSH_PROMISE on stream " + streamId);
|
|
}
|
|
// always decode the headers as they may affect connection-level HPACK
|
|
// decoding state
|
|
assert pushContinuationState == null;
|
|
PushPromiseDecoder decoder = new PushPromiseDecoder(streamId, promisedStreamid, parent);
|
|
decodeHeaders(pp, decoder);
|
|
if (pp.endHeaders()) {
|
|
if (decoder.errorRef.get() == null) {
|
|
completePushPromise(promisedStreamid, parent, decoder.headers());
|
|
}
|
|
} else {
|
|
pushContinuationState = new PushContinuationState(decoder, pp);
|
|
}
|
|
}
|
|
|
|
private <T> void handlePushContinuation(PushContinuationState pcs, Stream<T> parent, ContinuationFrame cf)
|
|
throws IOException {
|
|
assert pcs.pushContFrame.streamid() == cf.streamid() : String.format(
|
|
"Received CONTINUATION on a different stream %s != %s",
|
|
cf.streamid(), pcs.pushContFrame.streamid());
|
|
decodeHeaders(cf, pcs.pushContDecoder);
|
|
// if all continuations are sent, set pushWithContinuation to null
|
|
if (cf.endHeaders()) {
|
|
if (pcs.pushContDecoder.errorRef.get() == null) {
|
|
completePushPromise(pcs.pushContFrame.getPromisedStream(), parent,
|
|
pcs.pushContDecoder.headers());
|
|
}
|
|
pushContinuationState = null;
|
|
}
|
|
}
|
|
|
|
private <T> void completePushPromise(int promisedStreamid, Stream<T> parent, HttpHeaders headers)
|
|
throws IOException {
|
|
if (parent == null) {
|
|
resetStream(promisedStreamid, ResetFrame.REFUSED_STREAM);
|
|
return;
|
|
}
|
|
HttpRequestImpl parentReq = parent.request;
|
|
if (promisedStreamid < nextPushStream) {
|
|
// From RFC 9113 section 5.1.1:
|
|
// The identifier of a newly established stream MUST be numerically
|
|
// greater than all streams that the initiating endpoint has
|
|
// opened or reserved.
|
|
protocolError(ResetFrame.PROTOCOL_ERROR, String.format(
|
|
"Unexpected stream identifier: %s < %s", promisedStreamid, nextPushStream));
|
|
return;
|
|
}
|
|
if (promisedStreamid != nextPushStream) {
|
|
// we don't support skipping stream ids;
|
|
resetStream(promisedStreamid, ResetFrame.PROTOCOL_ERROR);
|
|
return;
|
|
} else if (!reserveStream(false, true)) {
|
|
resetStream(promisedStreamid, ResetFrame.REFUSED_STREAM);
|
|
return;
|
|
} else {
|
|
nextPushStream += 2;
|
|
}
|
|
|
|
HttpRequestImpl pushReq = HttpRequestImpl.createPushRequest(parentReq, headers);
|
|
Exchange<T> pushExch = new Exchange<>(pushReq, parent.exchange.multi);
|
|
Stream.PushedStream<T> pushStream = createPushStream(parent, pushExch);
|
|
pushExch.exchImpl = pushStream;
|
|
pushStream.registerStream(promisedStreamid, true);
|
|
parent.incoming_pushPromise(pushReq, pushStream);
|
|
}
|
|
|
|
private void handleConnectionFrame(Http2Frame frame)
|
|
throws IOException
|
|
{
|
|
switch (frame.type()) {
|
|
case SettingsFrame.TYPE -> handleSettings((SettingsFrame) frame);
|
|
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, "unknown frame: " + frame);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns true if this connection hasn't been terminated and the underlying
|
|
* {@linkplain NetworkChannel#isOpen() channel is open}. false otherwise.
|
|
*/
|
|
final boolean isOpen() {
|
|
return this.connTerminator.terminationCause.get() == null && connection.channel().isOpen();
|
|
}
|
|
|
|
void resetStream(int streamid, int code) {
|
|
try {
|
|
if (connection.channel().isOpen()) {
|
|
// no need to try & send a reset frame if the
|
|
// connection channel is already closed.
|
|
Log.logError(
|
|
"Resetting stream {0,number,integer} with error code {1,number,integer}",
|
|
streamid, code);
|
|
markStream(streamid, code);
|
|
ResetFrame frame = new ResetFrame(streamid, code);
|
|
sendFrame(frame);
|
|
} else if (debug.on()) {
|
|
debug.log("Channel already closed, no need to reset stream %d",
|
|
streamid);
|
|
}
|
|
} finally {
|
|
decrementStreamsCount(streamid);
|
|
closeStream(streamid);
|
|
}
|
|
}
|
|
|
|
private void markStream(int streamid, int code) {
|
|
Stream<?> s = streams.get(streamid);
|
|
if (s != null) s.markStream(code);
|
|
}
|
|
|
|
// reduce count of streams by 1 if stream still exists
|
|
void decrementStreamsCount(int streamid) {
|
|
stateLock.lock();
|
|
try {
|
|
decrementStreamsCount0(streamid);
|
|
} finally {
|
|
stateLock.unlock();
|
|
}
|
|
|
|
}
|
|
|
|
private void decrementStreamsCount0(int streamid) {
|
|
Stream<?> s = streams.get(streamid);
|
|
if (s == null || !s.deRegister())
|
|
return;
|
|
if (streamid % 2 == 1) {
|
|
numReservedClientStreams--;
|
|
assert numReservedClientStreams >= 0 :
|
|
"negative client stream count for stream=" + streamid;
|
|
} else {
|
|
numReservedServerStreams--;
|
|
assert numReservedServerStreams >= 0 :
|
|
"negative server stream count for stream=" + streamid;
|
|
}
|
|
}
|
|
|
|
// This method is called when the HTTP/2 client is being
|
|
// stopped. Do not call it from anywhere else.
|
|
void closeAllStreams() {
|
|
if (debug.on()) debug.log("Close all streams");
|
|
for (var streamId : streams.keySet()) {
|
|
// safe to call without locking - see Stream::deRegister
|
|
decrementStreamsCount(streamId);
|
|
closeStream(streamId);
|
|
}
|
|
}
|
|
|
|
// Check if this is the last stream aside from stream 0,
|
|
// arm timeout
|
|
void closeStream(int streamid) {
|
|
if (debug.on()) debug.log("Closed stream %d", streamid);
|
|
|
|
Stream<?> s;
|
|
stateLock.lock();
|
|
try {
|
|
s = streams.remove(streamid);
|
|
if (s != null) {
|
|
// decrement the reference count on the HttpClientImpl
|
|
// to allow the SelectorManager thread to exit if no
|
|
// other operation is pending and the facade is no
|
|
// longer referenced.
|
|
client().streamUnreference();
|
|
}
|
|
} finally {
|
|
stateLock.unlock();
|
|
}
|
|
// ## Remove s != null. It is a hack for delayed cancellation,reset
|
|
if (s != null && !(s instanceof Stream.PushedStream)) {
|
|
// Since PushStreams have no request body, then they have no
|
|
// corresponding entry in the window controller.
|
|
windowController.removeStream(streamid);
|
|
}
|
|
if (shouldClose()) {
|
|
close();
|
|
} else {
|
|
// Start timer if property present and not already created
|
|
stateLock.lock();
|
|
try {
|
|
// idleConnectionTimeoutEvent is always accessed within a lock protected block
|
|
if (streams.isEmpty() && idleConnectionTimeoutEvent == null) {
|
|
final HttpClient.Version version = Version.HTTP_2;
|
|
idleConnectionTimeoutEvent = client().idleConnectionTimeout(version)
|
|
.map(IdleConnectionTimeoutEvent::new)
|
|
.orElse(null);
|
|
if (idleConnectionTimeoutEvent != null) {
|
|
client().registerTimer(idleConnectionTimeoutEvent);
|
|
}
|
|
}
|
|
} finally {
|
|
stateLock.unlock();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Increments this connection's send Window by the amount in the given frame.
|
|
*/
|
|
private void handleWindowUpdate(WindowUpdateFrame f) {
|
|
int amount = f.getUpdate();
|
|
if (amount <= 0) {
|
|
// ## temporarily disable to workaround a bug in Jetty where it
|
|
// ## sends Window updates with a 0 update value.
|
|
//protocolError(ErrorFrame.PROTOCOL_ERROR);
|
|
} else {
|
|
boolean success = windowController.increaseConnectionWindow(amount);
|
|
if (!success) {
|
|
protocolError(ErrorFrame.FLOW_CONTROL_ERROR, null); // overflow
|
|
}
|
|
}
|
|
}
|
|
|
|
private void protocolError(final int errorCode, final String msg) {
|
|
final Http2TerminationCause terminationCause =
|
|
Http2TerminationCause.forH2Error(errorCode, msg);
|
|
framesDecoder.close(terminationCause.getLogMsg());
|
|
close(terminationCause);
|
|
}
|
|
|
|
private void handleSettings(SettingsFrame frame) {
|
|
assert frame.streamid() == 0;
|
|
if (!frame.getFlag(SettingsFrame.ACK)) {
|
|
int newWindowSize = frame.getParameter(INITIAL_WINDOW_SIZE);
|
|
if (newWindowSize != -1) {
|
|
int oldWindowSize = serverSettings.getParameter(INITIAL_WINDOW_SIZE);
|
|
int diff = newWindowSize - oldWindowSize;
|
|
if (diff != 0) {
|
|
windowController.adjustActiveStreams(diff);
|
|
}
|
|
}
|
|
|
|
serverSettings.update(frame);
|
|
sendFrame(new SettingsFrame(SettingsFrame.ACK));
|
|
}
|
|
}
|
|
|
|
private void handlePing(PingFrame frame) {
|
|
frame.setFlag(PingFrame.ACK);
|
|
sendUnorderedFrame(frame);
|
|
}
|
|
|
|
private void handleGoAway(final GoAwayFrame frame) {
|
|
final long lastProcessedStream = frame.getLastStream();
|
|
assert lastProcessedStream >= 0 : "unexpected last stream id: "
|
|
+ lastProcessedStream + " in GOAWAY frame";
|
|
|
|
goAwayRecvd.set(true);
|
|
setFinalStream(); // don't allow any new streams on this connection
|
|
if (debug.on()) {
|
|
debug.log("processing incoming GOAWAY with last processed stream id:%s in frame %s",
|
|
lastProcessedStream, frame);
|
|
}
|
|
// see if this connection has previously received a GOAWAY from the peer and if yes
|
|
// then check if this new last processed stream id is lesser than the previous
|
|
// known last processed stream id. Only update the last processed stream id if the new
|
|
// one is lesser than the previous one.
|
|
long prevLastProcessed = lastProcessedStreamInGoAway.get();
|
|
while (prevLastProcessed == -1 || lastProcessedStream < prevLastProcessed) {
|
|
if (lastProcessedStreamInGoAway.compareAndSet(prevLastProcessed,
|
|
lastProcessedStream)) {
|
|
break;
|
|
}
|
|
prevLastProcessed = lastProcessedStreamInGoAway.get();
|
|
}
|
|
handlePeerUnprocessedStreams(lastProcessedStreamInGoAway.get());
|
|
}
|
|
|
|
private void handlePeerUnprocessedStreams(final long lastProcessedStream) {
|
|
final AtomicInteger numClosed = new AtomicInteger(); // atomic merely to allow usage within lambda
|
|
streams.forEach((id, exchange) -> {
|
|
if (id > lastProcessedStream) {
|
|
// any streams with an stream id higher than the last processed stream
|
|
// can be retried (on a new connection). we close the exchange as unprocessed
|
|
// to facilitate the retrying.
|
|
client2.client().theExecutor().ensureExecutedAsync(exchange::closeAsUnprocessed);
|
|
numClosed.incrementAndGet();
|
|
}
|
|
});
|
|
if (debug.on()) {
|
|
debug.log(numClosed.get() + " stream(s), with id greater than " + lastProcessedStream
|
|
+ ", will be closed as unprocessed");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Max frame size we are allowed to send
|
|
*/
|
|
public int getMaxSendFrameSize() {
|
|
int param = serverSettings.getParameter(MAX_FRAME_SIZE);
|
|
if (param == -1) {
|
|
param = DEFAULT_FRAME_SIZE;
|
|
}
|
|
return param;
|
|
}
|
|
|
|
/**
|
|
* Max frame size we will receive
|
|
*/
|
|
public int getMaxReceiveFrameSize() {
|
|
return clientSettings.getParameter(MAX_FRAME_SIZE);
|
|
}
|
|
|
|
private static final String CLIENT_PREFACE = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n";
|
|
|
|
private static final byte[] PREFACE_BYTES =
|
|
CLIENT_PREFACE.getBytes(StandardCharsets.ISO_8859_1);
|
|
|
|
/**
|
|
* Sends Connection preface and Settings frame with current preferred
|
|
* values
|
|
*/
|
|
private void sendConnectionPreface() throws IOException {
|
|
Log.logTrace("{0}: start sending connection preface to {1}",
|
|
connection.channel().getLocalAddress(),
|
|
connection.address());
|
|
SettingsFrame sf = new SettingsFrame(clientSettings);
|
|
ByteBuffer buf = framesEncoder.encodeConnectionPreface(PREFACE_BYTES, sf);
|
|
Log.logFrames(sf, "OUT");
|
|
// send preface bytes and SettingsFrame together
|
|
HttpPublisher publisher = publisher();
|
|
publisher.enqueueUnordered(List.of(buf));
|
|
publisher.signalEnqueued();
|
|
// mark preface sent.
|
|
framesController.markPrefaceSent();
|
|
Log.logTrace("PREFACE_BYTES sent");
|
|
Log.logTrace("Settings Frame sent");
|
|
|
|
// send a Window update for the receive buffer we are using
|
|
// minus the initial 64 K -1 specified in protocol:
|
|
// RFC 7540, Section 6.9.2:
|
|
// "[...] the connection flow-control window is set to the default
|
|
// initial window size until a WINDOW_UPDATE frame is received."
|
|
//
|
|
// Note that the default initial window size, not to be confused
|
|
// with the initial window size, is defined by RFC 7540 as
|
|
// 64K -1.
|
|
final int len = windowUpdater.initialWindowSize - INITIAL_CONNECTION_WINDOW_SIZE;
|
|
assert len >= 0;
|
|
if (len > 0) {
|
|
if (Log.channel()) {
|
|
Log.logChannel("Sending initial connection window update frame: {0} ({1} - {2})",
|
|
len, windowUpdater.initialWindowSize, INITIAL_CONNECTION_WINDOW_SIZE);
|
|
}
|
|
windowUpdater.sendWindowUpdate(len);
|
|
}
|
|
// there will be an ACK to the windows update - which should
|
|
// cause any pending data stored before the preface was sent to be
|
|
// flushed (see PrefaceController).
|
|
Log.logTrace("finished sending connection preface");
|
|
if (debug.on())
|
|
debug.log("Triggering processing of buffered data"
|
|
+ " after sending connection preface");
|
|
subscriber.onNext(List.of(EMPTY_TRIGGER));
|
|
}
|
|
|
|
/**
|
|
* Called to get the initial stream after a connection upgrade.
|
|
* If the stream was cancelled, it might no longer be in the
|
|
* stream map. Therefore - we use the initialStream field
|
|
* instead, and reset it to null after returning it.
|
|
* @param <T> the response type
|
|
* @return the initial stream created during the upgrade.
|
|
*/
|
|
@SuppressWarnings("unchecked")
|
|
<T> Stream<T> getInitialStream() {
|
|
var s = (Stream<T>) initialStream;
|
|
initialStream = null;
|
|
return s;
|
|
}
|
|
|
|
/**
|
|
* Returns an existing Stream with given id, or null if doesn't exist
|
|
*/
|
|
@SuppressWarnings("unchecked")
|
|
<T> Stream<T> getStream(int streamid) {
|
|
return (Stream<T>)streams.get(streamid);
|
|
}
|
|
|
|
/**
|
|
* Creates Stream with given id.
|
|
*/
|
|
final <T> Stream<T> createStream(Exchange<T> exchange) {
|
|
Stream<T> stream = new Stream<>(this, exchange, windowController);
|
|
return stream;
|
|
}
|
|
|
|
<T> Stream.PushedStream<T> createPushStream(Stream<T> parent, Exchange<T> pushEx) {
|
|
PushGroup<T> pg = parent.exchange.getPushGroup();
|
|
return new Stream.PushedStream<>(parent, pg, this, pushEx);
|
|
}
|
|
|
|
/**
|
|
* Attempts to notify the idle connection management that this connection should
|
|
* be considered "in use". This way the idle connection management doesn't close
|
|
* this connection during the time the connection is handed out from the pool and any
|
|
* new stream created on that connection.
|
|
* @return true if the connection has been successfully reserved and is {@link #isOpen()}. false
|
|
* otherwise; in which case the connection must not be handed out from the pool.
|
|
*/
|
|
boolean tryReserveForPoolCheckout() {
|
|
// must be done with "stateLock" held to co-ordinate idle connection management
|
|
stateLock.lock();
|
|
try {
|
|
cancelIdleCloseEvent();
|
|
// consider the reservation successful only if the connection is open and
|
|
// hasn't been chosen for idle termination
|
|
return !this.connTerminator.isMarkedForIdleTermination() && isOpen();
|
|
} finally {
|
|
stateLock.unlock();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cancels any event that might have been scheduled to close this connection. Must be called
|
|
* with the stateLock held.
|
|
*/
|
|
private void cancelIdleCloseEvent() {
|
|
assert stateLock.isHeldByCurrentThread() : "Current thread doesn't hold " + stateLock;
|
|
if (idleConnectionTimeoutEvent == null) {
|
|
return;
|
|
}
|
|
idleConnectionTimeoutEvent.cancel();
|
|
idleConnectionTimeoutEvent = null;
|
|
}
|
|
|
|
<T> void putStream(Stream<T> stream, int streamid) {
|
|
// increment the reference count on the HttpClientImpl
|
|
// to prevent the SelectorManager thread from exiting until
|
|
// the stream is closed.
|
|
stateLock.lock();
|
|
try {
|
|
if (isOpen() && !this.connTerminator.isMarkedForIdleTermination()) {
|
|
if (debug.on()) {
|
|
debug.log("Opened stream %d", streamid);
|
|
}
|
|
client().streamReference();
|
|
streams.put(streamid, stream);
|
|
// don't consider the connection idle anymore
|
|
cancelIdleCloseEvent();
|
|
return;
|
|
}
|
|
} finally {
|
|
stateLock.unlock();
|
|
}
|
|
if (debug.on()) debug.log("connection closed: closing stream %d", stream);
|
|
final Http2TerminationCause terminationCause = getTerminationCause();
|
|
assert terminationCause != null : "termination cause is null";
|
|
stream.cancel(new IOException("Stream " + streamid + " cancelled", terminationCause.getCloseCause()));
|
|
}
|
|
|
|
/**
|
|
* Encode the headers into a List<ByteBuffer> and then create HEADERS
|
|
* and CONTINUATION frames from the list and return the List<Http2Frame>.
|
|
*/
|
|
private List<HeaderFrame> encodeHeaders(OutgoingHeaders<Stream<?>> frame) {
|
|
// max value of frame size is clamped by default frame size to avoid OOM
|
|
int bufferSize = Math.min(Math.max(getMaxSendFrameSize(), 1024), DEFAULT_FRAME_SIZE);
|
|
List<ByteBuffer> buffers = encodeHeadersImpl(
|
|
bufferSize,
|
|
frame.getAttachment().getRequestPseudoHeaders(),
|
|
frame.getUserHeaders(),
|
|
frame.getSystemHeaders());
|
|
|
|
List<HeaderFrame> frames = new ArrayList<>(buffers.size());
|
|
Iterator<ByteBuffer> bufIterator = buffers.iterator();
|
|
HeaderFrame oframe = new HeadersFrame(frame.streamid(), frame.getFlags(), bufIterator.next());
|
|
frames.add(oframe);
|
|
while(bufIterator.hasNext()) {
|
|
oframe = new ContinuationFrame(frame.streamid(), bufIterator.next());
|
|
frames.add(oframe);
|
|
}
|
|
oframe.setFlag(HeaderFrame.END_HEADERS);
|
|
return frames;
|
|
}
|
|
|
|
// Dedicated cache for headers encoding ByteBuffer.
|
|
// There can be no concurrent access to this buffer as all access to this buffer
|
|
// and its content happen within a single critical code block section protected
|
|
// by the sendLock. / (see sendFrame())
|
|
// private final ByteBufferPool headerEncodingPool = new ByteBufferPool();
|
|
|
|
private ByteBuffer getHeaderBuffer(int size) {
|
|
ByteBuffer buf = ByteBuffer.allocate(size);
|
|
buf.limit(size);
|
|
return buf;
|
|
}
|
|
|
|
/*
|
|
* Encodes all the headers from the given HttpHeaders into the given List
|
|
* of buffers.
|
|
*
|
|
* From https://tools.ietf.org/html/rfc7540#section-8.1.2 :
|
|
*
|
|
* ...Just as in HTTP/1.x, header field names are strings of ASCII
|
|
* characters that are compared in a case-insensitive fashion. However,
|
|
* header field names MUST be converted to lowercase prior to their
|
|
* encoding in HTTP/2...
|
|
*/
|
|
private List<ByteBuffer> encodeHeadersImpl(int bufferSize, HttpHeaders... headers) {
|
|
ByteBuffer buffer = getHeaderBuffer(bufferSize);
|
|
List<ByteBuffer> buffers = new ArrayList<>();
|
|
for (HttpHeaders header : headers) {
|
|
for (Map.Entry<String, List<String>> e : header.map().entrySet()) {
|
|
String lKey = e.getKey().toLowerCase(Locale.US);
|
|
List<String> values = e.getValue();
|
|
for (String value : values) {
|
|
hpackOut.header(lKey, value);
|
|
while (!hpackOut.encode(buffer)) {
|
|
if (!buffer.hasRemaining()) {
|
|
buffer.flip();
|
|
buffers.add(buffer);
|
|
buffer = getHeaderBuffer(bufferSize);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
buffer.flip();
|
|
buffers.add(buffer);
|
|
return buffers;
|
|
}
|
|
|
|
|
|
private List<ByteBuffer> encodeHeaders(OutgoingHeaders<Stream<?>> oh, Stream<?> stream) {
|
|
oh.streamid(stream.streamid);
|
|
if (Log.headers()) {
|
|
StringBuilder sb = new StringBuilder("HEADERS FRAME (streamid=%s):\n".formatted(stream.streamid));
|
|
sb.append(" %s %s\n".formatted(stream.request.method(), stream.request.uri()));
|
|
Log.dumpHeaders(sb, " ", oh.getAttachment().getRequestPseudoHeaders());
|
|
Log.dumpHeaders(sb, " ", oh.getSystemHeaders());
|
|
Log.dumpHeaders(sb, " ", oh.getUserHeaders());
|
|
Log.logHeaders(sb.toString());
|
|
}
|
|
List<HeaderFrame> frames = encodeHeaders(oh);
|
|
return encodeFrames(frames);
|
|
}
|
|
|
|
private List<ByteBuffer> encodeFrames(List<HeaderFrame> frames) {
|
|
if (Log.frames()) {
|
|
frames.forEach(f -> Log.logFrames(f, "OUT"));
|
|
}
|
|
return framesEncoder.encodeFrames(frames);
|
|
}
|
|
|
|
private Stream<?> registerNewStream(OutgoingHeaders<Stream<?>> oh) {
|
|
Stream<?> stream = oh.getAttachment();
|
|
assert stream.streamid == 0;
|
|
int streamid = nextstreamid;
|
|
Throwable cause = null;
|
|
synchronized (this) {
|
|
if (!isOpen()) {
|
|
final Http2TerminationCause terminationCause = getTerminationCause();
|
|
assert terminationCause != null : "termination cause is null";
|
|
cause = terminationCause.getCloseCause();
|
|
}
|
|
}
|
|
if (cause != null) {
|
|
stream.cancelImpl(cause);
|
|
return null;
|
|
}
|
|
if (stream.registerStream(streamid, false)) {
|
|
// set outgoing window here. This allows thread sending
|
|
// body to proceed.
|
|
nextstreamid += 2;
|
|
windowController.registerStream(streamid, getInitialSendWindowSize());
|
|
return stream;
|
|
} else {
|
|
stream.cancelImpl(new IOException("Request cancelled"));
|
|
if (shouldClose()) {
|
|
close();
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private final Lock sendlock = new ReentrantLock();
|
|
|
|
void sendFrame(Http2Frame frame) {
|
|
try {
|
|
if (debug.on()) debug.log("sending frame: " + frame);
|
|
HttpPublisher publisher = publisher();
|
|
sendlock.lock();
|
|
try {
|
|
if (frame instanceof OutgoingHeaders) {
|
|
@SuppressWarnings("unchecked")
|
|
OutgoingHeaders<Stream<?>> oh = (OutgoingHeaders<Stream<?>>) frame;
|
|
Stream<?> stream = registerNewStream(oh);
|
|
// provide protection from inserting unordered frames between Headers and Continuation
|
|
if (stream != null) {
|
|
// we are creating a new stream: reset orphaned header count
|
|
orphanedHeaders.set(0);
|
|
publisher.enqueue(encodeHeaders(oh, stream));
|
|
}
|
|
} else {
|
|
publisher.enqueue(encodeFrame(frame));
|
|
}
|
|
} finally {
|
|
sendlock.unlock();
|
|
}
|
|
publisher.signalEnqueued();
|
|
} catch (IOException e) {
|
|
if (!client2.stopping()) {
|
|
Log.logError(e);
|
|
} else if (debug.on()) {
|
|
debug.log("Failed to send %s while stopping: %s", frame, e);
|
|
}
|
|
close(Http2TerminationCause.forException(e));
|
|
}
|
|
}
|
|
|
|
private List<ByteBuffer> encodeFrame(Http2Frame frame) {
|
|
Log.logFrames(frame, "OUT");
|
|
return framesEncoder.encodeFrame(frame);
|
|
}
|
|
|
|
void sendDataFrame(DataFrame frame) {
|
|
try {
|
|
HttpPublisher publisher = publisher();
|
|
publisher.enqueue(encodeFrame(frame));
|
|
publisher.signalEnqueued();
|
|
} catch (IOException e) {
|
|
if (!client2.stopping()) {
|
|
Log.logError(e);
|
|
} else if (debug.on()) {
|
|
debug.log("Failed to send %s while stopping: %s", frame, e);
|
|
}
|
|
close(Http2TerminationCause.forException(e));
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Direct call of the method bypasses locking on "sendlock" and
|
|
* allowed only of control frames: WindowUpdateFrame, PingFrame and etc.
|
|
* prohibited for such frames as DataFrame, HeadersFrame, ContinuationFrame.
|
|
*/
|
|
void sendUnorderedFrame(Http2Frame frame) {
|
|
try {
|
|
HttpPublisher publisher = publisher();
|
|
publisher.enqueueUnordered(encodeFrame(frame));
|
|
publisher.signalEnqueued();
|
|
} catch (IOException e) {
|
|
if (!client2.stopping()) {
|
|
Log.logError(e);
|
|
} else if (debug.on()) {
|
|
debug.log("Failed to send %s while stopping: %s", frame, e);
|
|
}
|
|
close(Http2TerminationCause.forException(e));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A simple tube subscriber for reading from the connection flow.
|
|
*/
|
|
final class Http2TubeSubscriber implements TubeSubscriber {
|
|
private volatile Flow.Subscription subscription;
|
|
private volatile boolean completed;
|
|
private volatile boolean dropped;
|
|
private final AtomicReference<Throwable> errorRef = new AtomicReference<>();
|
|
private final ConcurrentLinkedQueue<ByteBuffer> queue
|
|
= new ConcurrentLinkedQueue<>();
|
|
private final SequentialScheduler scheduler =
|
|
SequentialScheduler.lockingScheduler(this::processQueue);
|
|
private final HttpClientImpl client;
|
|
|
|
Http2TubeSubscriber(HttpClientImpl client) {
|
|
this.client = Objects.requireNonNull(client);
|
|
}
|
|
|
|
final void processQueue() {
|
|
try {
|
|
while (!queue.isEmpty() && !scheduler.isStopped()) {
|
|
ByteBuffer buffer = queue.poll();
|
|
assert buffer != null : "null buffer obtained from non-empty queue";
|
|
if (debug.on())
|
|
debug.log("sending %d to Http2Connection.asyncReceive",
|
|
buffer.remaining());
|
|
asyncReceive(buffer);
|
|
}
|
|
} catch (Throwable t) {
|
|
errorRef.compareAndSet(null, t);
|
|
} finally {
|
|
Throwable x = errorRef.get();
|
|
// if there was any error or if the TubeSubscriber completed normally,
|
|
// then close the connection
|
|
if (x != null || completed) {
|
|
// although the connection terminator stops the scheduler too,
|
|
// we don't want to wait that "long" and instead we should immediately
|
|
// stop the scheduler so that we don't enter "processQueue" anymore.
|
|
scheduler.stop();
|
|
if (client2.stopping()) {
|
|
if (debug.on()) {
|
|
debug.log("Stopping scheduler");
|
|
}
|
|
} else {
|
|
if (debug.on()) {
|
|
debug.log("Stopping scheduler", x);
|
|
}
|
|
}
|
|
// terminate the connection
|
|
final Http2TerminationCause tc = (x != null)
|
|
? Http2TerminationCause.forException(x)
|
|
: Http2TerminationCause.noErrorTermination();
|
|
Http2Connection.this.close(tc);
|
|
}
|
|
}
|
|
}
|
|
|
|
private final void runOrSchedule() {
|
|
if (client.isSelectorThread()) {
|
|
scheduler.runOrSchedule(client.theExecutor());
|
|
} else scheduler.runOrSchedule();
|
|
}
|
|
|
|
@Override
|
|
public void onSubscribe(Flow.Subscription subscription) {
|
|
// supports being called multiple time.
|
|
// doesn't cancel the previous subscription, since that is
|
|
// most probably the same as the new subscription.
|
|
assert this.subscription == null || dropped == false;
|
|
this.subscription = subscription;
|
|
dropped = false;
|
|
// TODO FIXME: request(1) should be done by the delegate.
|
|
if (!completed) {
|
|
if (debug.on())
|
|
debug.log("onSubscribe: requesting Long.MAX_VALUE for reading");
|
|
subscription.request(Long.MAX_VALUE);
|
|
} else {
|
|
if (debug.on()) debug.log("onSubscribe: already completed");
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onNext(List<ByteBuffer> item) {
|
|
if (completed) return;
|
|
if (debug.on()) debug.log(() -> "onNext: got " + Utils.remaining(item)
|
|
+ " bytes in " + item.size() + " buffers");
|
|
queue.addAll(item);
|
|
runOrSchedule();
|
|
}
|
|
|
|
@Override
|
|
public void onError(Throwable throwable) {
|
|
if (completed) return;
|
|
if (debug.on()) debug.log(() -> "onError: " + throwable);
|
|
errorRef.compareAndSet(null, throwable);
|
|
completed = true;
|
|
runOrSchedule();
|
|
}
|
|
|
|
@Override
|
|
public void onComplete() {
|
|
if (completed) return;
|
|
if (isActive()) {
|
|
final String msg = "EOF reached while reading";
|
|
errorRef.compareAndSet(null, new EOFException(msg));
|
|
if (debug.on()) {
|
|
debug.log(msg);
|
|
}
|
|
} else {
|
|
if (debug.on()) {
|
|
debug.log("HTTP/2 connection (with no active streams) closed by peer");
|
|
}
|
|
}
|
|
completed = true;
|
|
runOrSchedule();
|
|
}
|
|
|
|
@Override
|
|
public void dropSubscription() {
|
|
if (debug.on()) debug.log("dropSubscription");
|
|
// we could probably set subscription to null here...
|
|
// then we might not need the 'dropped' boolean?
|
|
dropped = true;
|
|
}
|
|
|
|
private void close() {
|
|
scheduler.stop();
|
|
queue.clear();
|
|
if (subscription != null) {
|
|
subscription.cancel();
|
|
}
|
|
queue.clear();
|
|
}
|
|
}
|
|
|
|
boolean isActive() {
|
|
stateLock.lock();
|
|
try {
|
|
return numReservedClientStreams > 0 || numReservedServerStreams > 0;
|
|
} finally {
|
|
stateLock.unlock();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public final String toString() {
|
|
return dbgString();
|
|
}
|
|
|
|
final String dbgString() {
|
|
return "Http2Connection("
|
|
+ connection.getConnectionFlow() + ")";
|
|
}
|
|
|
|
static final class ConnectionWindowUpdateSender extends WindowUpdateSender {
|
|
|
|
final int initialWindowSize;
|
|
|
|
public ConnectionWindowUpdateSender(Http2Connection connection,
|
|
int initialWindowSize) {
|
|
super(connection, initialWindowSize);
|
|
this.initialWindowSize = initialWindowSize;
|
|
}
|
|
|
|
@Override
|
|
int getStreamId() {
|
|
return 0;
|
|
}
|
|
|
|
@Override
|
|
protected boolean windowSizeExceeded(long received) {
|
|
if (connection.isOpen()) {
|
|
connection.protocolError(ErrorFrame.FLOW_CONTROL_ERROR,
|
|
"connection window exceeded (%s > %s)"
|
|
.formatted(received, windowSize));
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Thrown when https handshake negotiates http/1.1 alpn instead of h2
|
|
*/
|
|
static final class ALPNException extends IOException {
|
|
private static final long serialVersionUID = 0L;
|
|
final transient AbstractAsyncSSLConnection connection;
|
|
|
|
ALPNException(String msg, AbstractAsyncSSLConnection connection) {
|
|
super(msg);
|
|
this.connection = connection;
|
|
}
|
|
|
|
AbstractAsyncSSLConnection getConnection() {
|
|
return connection;
|
|
}
|
|
}
|
|
|
|
private void sendGoAway(final GoAwayFrame goAway) {
|
|
// currently we send a GOAWAY just once irrespective of what value the
|
|
// last stream id was in the GOAWAY frame
|
|
if (!goAwaySent.compareAndSet(false, true)) {
|
|
// already sent
|
|
return;
|
|
}
|
|
if (Log.trace()) {
|
|
Log.logTrace("{0} sending GOAWAY {1}", connection, goAway);
|
|
} else if (debug.on()) {
|
|
debug.log("sending GOAWAY " + goAway);
|
|
}
|
|
// this merely enqueues the frame
|
|
sendFrame(goAway);
|
|
}
|
|
|
|
/**
|
|
* Returns the termination cause if the connection is closed, else returns null.
|
|
*/
|
|
final Http2TerminationCause getTerminationCause() {
|
|
return this.connTerminator.determineTerminationCause();
|
|
}
|
|
|
|
// Responsible for doing all the necessary work for closing a Http2Connection
|
|
private final class Terminator {
|
|
// the cause for closing the connection. Must only be set in the
|
|
// Terminator.terminate(Http2TerminationCause) method.
|
|
private final AtomicReference<Http2TerminationCause> terminationCause = new AtomicReference<>();
|
|
// true if it has been decided to terminate the connection due to being idle,
|
|
// false otherwise. should be accessed only when holding the stateLock
|
|
private boolean chosenForIdleTermination;
|
|
|
|
private void terminate(final Http2TerminationCause terminationCause) {
|
|
Objects.requireNonNull(terminationCause, "termination cause cannot be null");
|
|
// allow to be terminated only once
|
|
stateLock.lock();
|
|
try {
|
|
final boolean success = this.terminationCause.compareAndSet(null, terminationCause);
|
|
if (!success) {
|
|
// already terminated or is being terminated by some other thread
|
|
return;
|
|
}
|
|
// disable the idle timeout event, since we are now going to terminate the
|
|
// connection
|
|
Http2Connection.this.cancelIdleCloseEvent();
|
|
} finally {
|
|
stateLock.unlock();
|
|
}
|
|
// do the actual termination
|
|
doTerminate();
|
|
}
|
|
|
|
private void doTerminate() {
|
|
final Http2TerminationCause tc = terminationCause.get();
|
|
assert tc != null : "missing termination cause";
|
|
// we send a GOAWAY frame only if the remote side hasn't already indicated
|
|
// the intention to close the connection by previously sending a GOAWAY of its own
|
|
if (!Http2Connection.this.goAwayRecvd.get()) {
|
|
final int lastStream = 0; // TODO: set last stream. For now zero is ok.
|
|
final String peerVisibleReason = tc.getPeerVisibleReason();
|
|
final GoAwayFrame goAway;
|
|
if (peerVisibleReason == null) {
|
|
goAway = new GoAwayFrame(lastStream, tc.getCloseCode());
|
|
} else {
|
|
goAway = new GoAwayFrame(lastStream, tc.getCloseCode(),
|
|
peerVisibleReason.getBytes(UTF_8));
|
|
}
|
|
sendGoAway(goAway);
|
|
}
|
|
// now close the connection
|
|
|
|
if (Log.errors() || debug.on()) {
|
|
final String stateStr = "Abnormal close=" + tc.isAbnormalClose() +
|
|
", has active streams=" + isActive() +
|
|
", GOAWAY received=" + goAwayRecvd.get() +
|
|
", GOAWAY sent=" + goAwaySent.get();
|
|
if (Log.errors()) {
|
|
Log.logError("Closing connection {0} ({1}) due to: {2}",
|
|
connection, stateStr, tc);
|
|
} else {
|
|
debug.log("Closing connection (" + stateStr + ") due to: " + tc);
|
|
}
|
|
}
|
|
// close the TubeSubscriber
|
|
subscriber.close();
|
|
client2.removeFromPool(Http2Connection.this);
|
|
// notify the HTTP/2 streams of the connection closure
|
|
for (final Stream<?> s : streams.values()) {
|
|
try {
|
|
s.connectionClosing(tc.getCloseCause());
|
|
} catch (Throwable e) {
|
|
Log.logError("Failed to close stream {0}: {1}", s.streamid, e);
|
|
}
|
|
}
|
|
// close the underlying connection
|
|
connection.close(tc.getCloseCause());
|
|
}
|
|
|
|
private void markForIdleTermination() {
|
|
assert stateLock.isHeldByCurrentThread() : Thread.currentThread()
|
|
+ " not holding stateLock";
|
|
this.chosenForIdleTermination = true;
|
|
}
|
|
|
|
private boolean isMarkedForIdleTermination() {
|
|
assert stateLock.isHeldByCurrentThread() : Thread.currentThread()
|
|
+ " not holding stateLock";
|
|
return this.chosenForIdleTermination;
|
|
}
|
|
|
|
private void idleTimedOut() {
|
|
if (debug.on()) {
|
|
debug.log("closing connection due to being idle");
|
|
}
|
|
this.terminate(Http2TerminationCause.idleTimedOut());
|
|
}
|
|
|
|
/**
|
|
* Returns the termination cause for the connection. This method guarantees that if the
|
|
* {@linkplain Http2Connection#isOpen() connection is not open}, when this method is called,
|
|
* then it returns a non-null termination cause. Returns null if the connection is open.
|
|
*/
|
|
private Http2TerminationCause determineTerminationCause() {
|
|
final Http2TerminationCause tc = this.terminationCause.get();
|
|
if (tc != null) {
|
|
// already terminated, return the cause
|
|
return tc;
|
|
}
|
|
if (!connection.channel().isOpen()) {
|
|
// if the underlying SocketChannel isn't open, then terminate the connection.
|
|
// that way when Http2Connection.isOpen() returns false in that situation, then this
|
|
// getTerminationCause() will return a termination cause.
|
|
terminate(Http2TerminationCause.forException(new ClosedChannelException()));
|
|
final Http2TerminationCause terminated = this.terminationCause.get();
|
|
assert terminated != null : "missing termination cause";
|
|
return terminated;
|
|
}
|
|
return null; // connection still open
|
|
}
|
|
}
|
|
}
|