java-topology/src/java.net.http/share/classes/jdk/internal/net/http/HttpConnection.java
russell@unturf.com 0a580b313d undefect. CWE-407 — 63 sites patched across 27 ecosystems
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.
2026-03-26 17:11:57 -04:00

665 lines
26 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.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.Comparator;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.Flow;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BiPredicate;
import java.net.http.HttpClient.Version;
import java.net.http.HttpHeaders;
import javax.net.ssl.SNIServerName;
import jdk.internal.net.http.common.Alpns;
import jdk.internal.net.http.common.Demand;
import jdk.internal.net.http.common.FlowTube;
import jdk.internal.net.http.common.Logger;
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;
/**
* Wraps socket channel layer and takes care of SSL also.
*
* Subtypes are:
* PlainHttpConnection: regular direct TCP connection to server
* PlainProxyConnection: plain text proxy connection
* 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", Utils.DEBUG);
public static final Comparator<HttpConnection> COMPARE_BY_ID
= Comparator.comparing(HttpConnection::id);
private static final AtomicLong LABEL_COUNTER = new AtomicLong();
/** The address this connection is connected to. Could be a server or a proxy. */
final InetSocketAddress address;
private final HttpClientImpl client;
private final TrailingOperations trailingOperations;
/**
* A unique identifier that provides a total order among instances.
*/
private final long id;
/**
* A label to identify the connection.
* <p>
* This label helps with associating multiple components participating in a
* connection. For instance, an {@link AsyncSSLConnection} and the
* {@link PlainHttpConnection} it wraps will share the same label.
* </p>
*/
private final String label;
private final Origin originServer;
HttpConnection(Origin originServer, InetSocketAddress address, HttpClientImpl client,
String label) {
this.originServer = originServer;
this.address = address;
this.client = client;
trailingOperations = new TrailingOperations();
this.id = newConnectionId(client);
this.label = label;
}
private static String nextLabel(String prefix) {
return prefix + LABEL_COUNTER.incrementAndGet();
}
/**
* {@return a label identifying the connection to facilitate
* {@link HttpResponse#connectionLabel() HttpResponse::connectionLabel}}
*/
public final String label() {
return label;
}
// This is overridden in tests
long newConnectionId(HttpClientImpl client) {
return client.newConnectionId();
}
private long id() {
return id;
}
private static final class TrailingOperations {
private final Map<CompletionStage<?>, Boolean> operations =
new IdentityHashMap<>();
void add(CompletionStage<?> cf) {
synchronized (operations) {
operations.put(cf, Boolean.TRUE);
cf.whenComplete((r,t)-> remove(cf));
}
}
boolean remove(CompletionStage<?> cf) {
synchronized (operations) {
return operations.remove(cf);
}
}
}
final void addTrailingOperation(CompletionStage<?> cf) {
trailingOperations.add(cf);
}
// final void removeTrailingOperation(CompletableFuture<?> cf) {
// trailingOperations.remove(cf);
// }
final HttpClientImpl client() {
return client;
}
/**
* 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);
/**
* Finishes the connection phase.
*
* Returns a CompletableFuture that completes when any additional,
* type specific, setup has been done. Must be called after connectAsync. */
public abstract CompletableFuture<Void> finishConnect();
/** Tells whether, or not, this connection is connected to its destination. */
abstract boolean connected();
/** Tells whether, or not, this connection is secure ( over SSL ) */
abstract boolean isSecure();
/**
* Tells whether, or not, this connection is proxied.
* Returns true for tunnel connections, or clear connection to
* any host through proxy.
*/
abstract boolean isProxied();
/**
* 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.
*/
abstract InetSocketAddress proxy();
/** Tells whether, or not, this connection is open. */
boolean isOpen() {
return channel().isOpen() &&
(connected() ? !isFlowFinished() : true);
}
/**
* {@return {@code true} if the {@linkplain #getConnectionFlow()
* connection flow} is {@linkplain FlowTube#isFinished() finished}.
*/
boolean isFlowFinished() {
return getConnectionFlow().isFinished();
}
/**
* Forces a call to the native implementation of the
* connection's channel to verify that this channel is still
* open.
* <p>
* This method should only be called just after an HTTP/1.1
* connection is retrieved from the HTTP/1.1 connection pool.
* It is used to trigger an early detection of the channel state,
* before handling the connection over to the HTTP stack.
* It helps minimizing race conditions where the selector manager
* thread hasn't woken up - or hasn't raised the event, before
* the connection was retrieved from the pool. It helps reduce
* the occurrence of "HTTP/1.1 parser received no bytes"
* exception, when the server closes the connection while
* it's being taken out of the pool.
* <p>
* This method attempts to read one byte from the underlying
* channel. Because the connection was in the pool - there
* should be nothing to read.
* <p>
* If {@code read} manages to read a byte off the connection, this is a
* protocol error: the method closes the connection and returns false.
* If {@code read} returns EOF, the method closes the connection and
* returns false.
* If {@code read} throws an exception, the method returns false.
* Otherwise, {@code read} returns 0, the channel appears to be
* still open, and the method returns true.
* @return true if the channel appears to be still open.
*/
boolean checkOpen() {
if (isOpen()) {
try {
// channel is non blocking
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());
return false;
}
}
return false;
}
/**
* {@return the {@link Origin} server against which this connection communicates.
* Returns {@code null} if the connection is a plain connection to a proxy}
*/
final Origin getOriginServer() {
return this.originServer;
}
interface HttpPublisher extends FlowTube.TubePublisher {
void enqueue(List<ByteBuffer> buffers) throws IOException;
void enqueueUnordered(List<ByteBuffer> buffers) throws IOException;
void signalEnqueued() throws IOException;
}
/**
* Returns the HTTP publisher associated with this connection. May be null
* if invoked before connecting.
*/
abstract HttpPublisher publisher();
/**
* Factory for retrieving HttpConnections. A connection can be retrieved
* from the connection pool, or a new one created if none available.
*
* The given {@code addr} is the ultimate destination. Any proxies,
* etc, are determined from the request. Returns a concrete instance which
* 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
* successfully ) renders the connection usable for requests.
*/
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
// unresolved. We must resolve the address before connecting to it.
InetSocketAddress proxy = Utils.resolveAddress(request.proxy());
HttpConnection c = null;
boolean secure = request.secure();
ConnectionPool pool = client.connectionPool();
if (!secure) {
c = pool.getConnection(false, addr, proxy);
if (c != null && c.checkOpen() /* may have been eof/closed when in the pool */) {
final HttpConnection conn = c;
if (DEBUG_LOGGER.on())
DEBUG_LOGGER.log(conn.getConnectionFlow()
+ ": plain connection retrieved from HTTP/1.1 pool");
return c;
} else {
return getPlainConnection(addr, proxy, request, client);
}
} else { // secure
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()) {
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 && client.hasRequiredHTTP2TLSVersion()) {
// 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);
}
}
}
private static HttpConnection getSSLConnection(InetSocketAddress addr,
InetSocketAddress proxy,
String[] alpn,
HttpRequestImpl request,
HttpClientImpl client) {
final String label = nextLabel("tls:");
final Origin originServer;
try {
originServer = Origin.from(request.uri());
} catch (IllegalArgumentException iae) {
// should never happen
throw new AssertionError("failed to determine origin server from request URI", iae);
}
if (proxy != null)
return new AsyncSSLTunnelConnection(originServer, addr, client, alpn, proxy,
proxyTunnelHeaders(request),
label);
else
return new AsyncSSLConnection(originServer, addr, client, alpn, label);
}
/**
* This method is used to build a filter that will accept or
* veto (header-name, value) tuple for transmission on the
* wire.
* The filter is applied to the headers when sending the headers
* to the remote party.
* Which tuple is accepted/vetoed depends on:
* <pre>
* - whether the connection is a tunnel connection
* [talking to a server through a proxy tunnel]
* - whether the method is CONNECT
* [establishing a CONNECT tunnel through a proxy]
* - whether the request is using a proxy
* (and the connection is not a tunnel)
* [talking to a server through a proxy]
* - whether the request is a direct connection to
* a server (no tunnel, no proxy).
* </pre>
* @param request
* @return
*/
BiPredicate<String,String> headerFilter(HttpRequestImpl request) {
if (isTunnel()) {
// talking to a server through a proxy tunnel
// don't send proxy-* headers to a plain server
assert !request.isConnect();
return Utils.NO_PROXY_HEADERS_FILTER;
} else if (request.isConnect()) {
// establishing a proxy tunnel
// check for proxy tunnel disabled schemes
// assert !this.isTunnel();
assert request.proxy() == null;
return Utils.PROXY_TUNNEL_FILTER;
} else if (request.proxy() != null) {
// talking to a server through a proxy (no tunnel)
// check for proxy disabled schemes
// assert !isTunnel() && !request.isConnect();
return Utils.PROXY_FILTER;
} else {
// talking to a server directly (no tunnel, no proxy)
// don't send proxy-* headers to a plain server
// assert request.proxy() == null && !request.isConnect();
return Utils.NO_PROXY_HEADERS_FILTER;
}
}
BiPredicate<String,String> contextRestricted(HttpRequestImpl request) {
if (!isTunnel() && request.isConnect()) {
// establishing a proxy tunnel
assert request.proxy() == null;
return Utils.PROXY_TUNNEL_RESTRICTED();
} else {
return Utils.ACCEPT_ALL;
}
}
// Composes a new immutable HttpHeaders that combines the
// user and system header but only keeps those headers that
// start with "proxy-"
private static ProxyHeaders proxyTunnelHeaders(HttpRequestImpl request) {
HttpHeaders userHeaders = HttpHeaders.of(request.headers().map(), Utils.PROXY_TUNNEL_FILTER);
HttpHeaders systemHeaders = HttpHeaders.of(request.getSystemHeadersBuilder().map(), Utils.PROXY_TUNNEL_FILTER);
return new ProxyHeaders(userHeaders, systemHeaders);
}
/* Returns either a plain HTTP connection or a plain tunnelling connection
* for proxied WebSocket */
private static HttpConnection getPlainConnection(InetSocketAddress addr,
InetSocketAddress proxy,
HttpRequestImpl request,
HttpClientImpl client) {
final String label = nextLabel("tcp:");
final Origin originServer;
try {
originServer = Origin.from(request.uri());
} catch (IllegalArgumentException iae) {
// should never happen
throw new AssertionError("failed to determine origin server from request URI", iae);
}
if (request.isWebSocket() && proxy != null)
return new PlainTunnelingConnection(originServer, addr, proxy, client,
proxyTunnelHeaders(request),
label);
if (proxy == null)
return new PlainHttpConnection(originServer, addr, client, label);
else
return new PlainProxyConnection(proxy, client, label);
}
void closeOrReturnToCache(HttpHeaders hdrs) {
if (hdrs == null) {
// the connection was closed by server, eof
Log.logTrace("Cannot return connection to pool: closing {0}", this);
close();
return;
}
HttpClientImpl client = client();
if (client == null) {
Log.logTrace("Client released: closing {0}", this);
close();
return;
}
ConnectionPool pool = client.connectionPool();
boolean keepAlive = hdrs.firstValue("Connection")
.map((s) -> !s.equalsIgnoreCase("close"))
.orElse(true);
if (keepAlive && isOpen()) {
Log.logTrace("Returning connection to the pool: {0}", this);
pool.returnToPool(this);
} else {
Log.logTrace("Closing connection (keepAlive={0}, isOpen={1}): {2}",
keepAlive, isOpen(), this);
close();
}
}
/* Tells whether or not this connection is a tunnel through a proxy */
boolean isTunnel() { return false; }
abstract NetworkChannel channel();
final InetSocketAddress address() {
return address;
}
/**
* Returns an unmodifiable list of {@link SNIServerName}s that were used during TLS handshake
* of this connection. If this connection doesn't represent a TLS based connection or if no SNI
* server names were used during the handshake, then this method returns an empty list.
*
* @return the SNI server names
*/
public List<SNIServerName> getSNIServerNames() {
return List.of();
}
abstract ConnectionPool.CacheKey cacheKey();
/**
* Closes this connection.
*/
@Override
public abstract void close();
/**
* Closes this connection due to the given cause.
* @param cause the cause for which the connection is closed, may be null
*/
abstract void close(Throwable cause);
/**
* {@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();
/**
* A publisher that makes it possible to publish (write) ordered (normal
* priority) and unordered (high priority) buffers downstream.
*/
final class PlainHttpPublisher implements HttpPublisher {
final Object reading;
PlainHttpPublisher() {
this(new Object());
}
PlainHttpPublisher(Object readingLock) {
this.reading = readingLock;
}
final ConcurrentLinkedDeque<List<ByteBuffer>> queue = new ConcurrentLinkedDeque<>();
final ConcurrentLinkedDeque<List<ByteBuffer>> priority = new ConcurrentLinkedDeque<>();
volatile Flow.Subscriber<? super List<ByteBuffer>> subscriber;
volatile HttpWriteSubscription subscription;
final SequentialScheduler writeScheduler =
new SequentialScheduler(this::flushTask);
@Override
public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> subscriber) {
synchronized (reading) {
//assert this.subscription == null;
//assert this.subscriber == null;
if (subscription == null) {
subscription = new HttpWriteSubscription();
}
this.subscriber = subscriber;
}
// TODO: should we do this in the flow?
subscriber.onSubscribe(subscription);
signal();
}
void flushTask(DeferredCompleter completer) {
try {
HttpWriteSubscription sub = subscription;
if (sub != null) sub.flush();
} finally {
completer.complete();
}
}
void signal() {
writeScheduler.runOrSchedule();
}
final class HttpWriteSubscription implements Flow.Subscription {
final Demand demand = new Demand();
@Override
public void request(long n) {
if (n <= 0) throw new IllegalArgumentException("non-positive request");
demand.increase(n);
if (debug.on())
debug.log("HttpPublisher: got request of " + n + " from "
+ getConnectionFlow());
writeScheduler.runOrSchedule();
}
@Override
public void cancel() {
if (debug.on())
debug.log("HttpPublisher: cancelled by " + getConnectionFlow());
}
private boolean isEmpty() {
return queue.isEmpty() && priority.isEmpty();
}
private List<ByteBuffer> poll() {
List<ByteBuffer> elem = priority.poll();
return elem == null ? queue.poll() : elem;
}
void flush() {
while (!isEmpty() && demand.tryDecrement()) {
List<ByteBuffer> elem = poll();
if (debug.on())
debug.log("HttpPublisher: sending "
+ Utils.remaining(elem) + " bytes ("
+ elem.size() + " buffers) to "
+ getConnectionFlow());
subscriber.onNext(elem);
}
}
}
@Override
public void enqueue(List<ByteBuffer> buffers) throws IOException {
queue.add(buffers);
int bytes = buffers.stream().mapToInt(ByteBuffer::remaining).sum();
debug.log("added %d bytes to the write queue", bytes);
}
@Override
public void enqueueUnordered(List<ByteBuffer> buffers) throws IOException {
// Unordered frames are sent before existing frames.
int bytes = buffers.stream().mapToInt(ByteBuffer::remaining).sum();
priority.add(buffers);
debug.log("added %d bytes in the priority write queue", bytes);
}
@Override
public void signalEnqueued() throws IOException {
debug.log("signalling the publisher of the write queue");
signal();
}
}
String dbgTag;
final String dbgString() {
FlowTube flow = getConnectionFlow();
String tag = dbgTag;
if (tag == null && flow != null) {
dbgTag = tag = this.getClass().getSimpleName() + "(" + flow + ")";
} else if (tag == null) {
tag = this.getClass().getSimpleName() + "(?)";
}
return tag;
}
@Override
public String toString() {
return "HttpConnection: " + channel().toString();
}
}