/
githubmirror
/
galahad
Обзор
Документация
Войти
/
githubmirror
/
galahad
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/java.net.http/share/classes/jdk/internal/net/http/HttpConnection.java
665 строк
26 KB
Daniel Jeliński
8371887: HttpClient: SSLParameters with no protocols configured disable HTTP2+ support
27 ноя 2025, 12:27
27 ноя 2025, 12:27
1f417e7
Код
Авторство
О чём код?
/* * 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(); } }