All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.eclipse.jetty.client.dynamic.HttpClientTransportDynamic Maven / Gradle / Ivy

//
// ========================================================================
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//

package org.eclipse.jetty.client.dynamic;

import java.io.IOException;
import java.net.SocketAddress;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.eclipse.jetty.alpn.client.ALPNClientConnection;
import org.eclipse.jetty.alpn.client.ALPNClientConnectionFactory;
import org.eclipse.jetty.client.AbstractConnectorHttpClientTransport;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.HttpClientTransport;
import org.eclipse.jetty.client.HttpDestination;
import org.eclipse.jetty.client.HttpRequest;
import org.eclipse.jetty.client.MultiplexConnectionPool;
import org.eclipse.jetty.client.MultiplexHttpDestination;
import org.eclipse.jetty.client.Origin;
import org.eclipse.jetty.client.http.HttpClientConnectionFactory;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.io.ClientConnectionFactory;
import org.eclipse.jetty.io.ClientConnector;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.EndPoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 

A {@link HttpClientTransport} that can dynamically switch among different application protocols.

*

Applications create HttpClientTransportDynamic instances specifying all the application protocols * it supports, in order of preference. The typical case is when the server supports both HTTP/1.1 and * HTTP/2, but the client does not know that. In this case, the application will create a * HttpClientTransportDynamic in this way:

*
 * ClientConnector clientConnector = new ClientConnector();
 * // Configure the clientConnector.
 *
 * // Prepare the application protocols.
 * ClientConnectionFactory.Info h1 = HttpClientConnectionFactory.HTTP11;
 * HTTP2Client http2Client = new HTTP2Client(clientConnector);
 * ClientConnectionFactory.Info h2 = new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client);
 *
 * // Create the HttpClientTransportDynamic, preferring h2 over h1.
 * HttpClientTransport transport = new HttpClientTransportDynamic(clientConnector, h2, h1);
 *
 * // Create the HttpClient.
 * client = new HttpClient(transport);
 * 
*

Note how in the code above the HttpClientTransportDynamic has been created with the application * protocols {@code h2} and {@code h1}, without the need to specify TLS (which is implied by the request * scheme) or ALPN (which is implied by HTTP/2 over TLS).

*

When a request is first sent, {@code (scheme, host, port)} are not enough to identify the destination * because the same origin may speak different protocols. * For example, the Jetty server supports speaking clear-text {@code http/1.1} and {@code h2c} on the same port. * Imagine a client sending a {@code h2c} request to that port; this will create a destination and connections * that speak {@code h2c}; it won't be possible to use the connections from that destination to send * {@code http/1.1} requests. * Therefore a destination is identified by a {@link org.eclipse.jetty.client.Origin} and * applications can customize the creation of the origin (for example depending on request protocol * version, or request headers, or request attributes, or even request path) by overriding * {@link HttpClientTransport#newOrigin(HttpRequest)}.

*/ public class HttpClientTransportDynamic extends AbstractConnectorHttpClientTransport { private static final Logger LOG = LoggerFactory.getLogger(HttpClientTransportDynamic.class); private final List factoryInfos; private final List protocols; /** * Creates a transport that speaks only HTTP/1.1. */ public HttpClientTransportDynamic() { this(HttpClientConnectionFactory.HTTP11); } public HttpClientTransportDynamic(ClientConnectionFactory.Info... factoryInfos) { this(findClientConnector(factoryInfos), factoryInfos); } /** * Creates a transport with the given {@link ClientConnector} and the given application protocols. * * @param connector the ClientConnector used by this transport * @param factoryInfos the application protocols that this transport can speak */ public HttpClientTransportDynamic(ClientConnector connector, ClientConnectionFactory.Info... factoryInfos) { super(connector); if (factoryInfos.length == 0) factoryInfos = new Info[]{HttpClientConnectionFactory.HTTP11}; this.factoryInfos = Arrays.asList(factoryInfos); this.protocols = Arrays.stream(factoryInfos) .flatMap(info -> Stream.concat(info.getProtocols(false).stream(), info.getProtocols(true).stream())) .distinct() .map(p -> p.toLowerCase(Locale.ENGLISH)) .collect(Collectors.toList()); Arrays.stream(factoryInfos).forEach(this::addBean); setConnectionPoolFactory(destination -> new MultiplexConnectionPool(destination, destination.getHttpClient().getMaxConnectionsPerDestination(), destination, 1)); } private static ClientConnector findClientConnector(ClientConnectionFactory.Info[] infos) { return Arrays.stream(infos) .flatMap(info -> info.getContainedBeans(ClientConnector.class).stream()) .findFirst() .orElseGet(ClientConnector::new); } @Override public Origin newOrigin(HttpRequest request) { boolean secure = HttpClient.isSchemeSecure(request.getScheme()); String http1 = "http/1.1"; String http2 = secure ? "h2" : "h2c"; List protocols = List.of(); if (request.isVersionExplicit()) { HttpVersion version = request.getVersion(); String desired = version == HttpVersion.HTTP_2 ? http2 : http1; if (this.protocols.contains(desired)) protocols = List.of(desired); } else { if (secure) { // There may be protocol negotiation, so preserve the order // of protocols chosen by the application. // We need to keep multiple protocols in case the protocol // is negotiated: e.g. [http/1.1, h2] negotiates [h2], but // here we don't know yet what will be negotiated. List http = List.of("http/1.1", "h2c", "h2"); protocols = this.protocols.stream() .filter(http::contains) .collect(Collectors.toCollection(ArrayList::new)); // The http/1.1 upgrade to http/2 over TLS implicitly // "negotiates" [h2c], so we need to remove [h2] // because we don't want to negotiate using ALPN. if (request.getHeaders().contains(HttpHeader.UPGRADE, "h2c")) protocols.remove("h2"); } else { // Pick the first. protocols = List.of(this.protocols.get(0)); } } Origin.Protocol protocol = null; if (!protocols.isEmpty()) protocol = new Origin.Protocol(protocols, secure && protocols.contains(http2)); return getHttpClient().createOrigin(request, protocol); } @Override public HttpDestination newHttpDestination(Origin origin) { SocketAddress address = origin.getAddress().getSocketAddress(); return new MultiplexHttpDestination(getHttpClient(), origin, getClientConnector().isIntrinsicallySecure(address)); } @Override public org.eclipse.jetty.io.Connection newConnection(EndPoint endPoint, Map context) throws IOException { HttpDestination destination = (HttpDestination)context.get(HTTP_DESTINATION_CONTEXT_KEY); Origin.Protocol protocol = destination.getOrigin().getProtocol(); ClientConnectionFactory factory; if (protocol == null) { // Use the default ClientConnectionFactory. factory = factoryInfos.get(0).getClientConnectionFactory(); } else { SocketAddress address = destination.getOrigin().getAddress().getSocketAddress(); boolean intrinsicallySecure = getClientConnector().isIntrinsicallySecure(address); if (!intrinsicallySecure && destination.isSecure() && protocol.isNegotiate()) { factory = new ALPNClientConnectionFactory(getClientConnector().getExecutor(), this::newNegotiatedConnection, protocol.getProtocols()); } else { factory = findClientConnectionFactoryInfo(protocol.getProtocols(), destination.isSecure()) .orElseThrow(() -> new IOException("Cannot find " + ClientConnectionFactory.class.getSimpleName() + " for " + protocol)) .getClientConnectionFactory(); } } return factory.newConnection(endPoint, context); } public void upgrade(EndPoint endPoint, Map context) { HttpDestination destination = (HttpDestination)context.get(HTTP_DESTINATION_CONTEXT_KEY); Origin.Protocol protocol = destination.getOrigin().getProtocol(); Info info = findClientConnectionFactoryInfo(protocol.getProtocols(), destination.isSecure()) .orElseThrow(() -> new IllegalStateException("Cannot find " + ClientConnectionFactory.class.getSimpleName() + " to upgrade to " + protocol)); info.upgrade(endPoint, context); } protected Connection newNegotiatedConnection(EndPoint endPoint, Map context) throws IOException { try { ALPNClientConnection alpnConnection = (ALPNClientConnection)endPoint.getConnection(); String protocol = alpnConnection.getProtocol(); Info factoryInfo; if (protocol != null) { if (LOG.isDebugEnabled()) LOG.debug("ALPN negotiated {} among {}", protocol, alpnConnection.getProtocols()); List protocols = List.of(protocol); factoryInfo = findClientConnectionFactoryInfo(protocols, true) .orElseThrow(() -> new IOException("Cannot find " + ClientConnectionFactory.class.getSimpleName() + " for negotiated protocol " + protocol)); } else { // Server does not support ALPN, let's try the first protocol. factoryInfo = factoryInfos.get(0); if (LOG.isDebugEnabled()) LOG.debug("No ALPN protocol, using {}", factoryInfo); } return factoryInfo.getClientConnectionFactory().newConnection(endPoint, context); } catch (Throwable failure) { this.connectFailed(context, failure); throw failure; } } private Optional findClientConnectionFactoryInfo(List protocols, boolean secure) { return factoryInfos.stream() .filter(info -> info.matches(protocols, secure)) .findFirst(); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy