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

org.eclipse.jetty.client.HttpProxy Maven / Gradle / Ivy

//
// ========================================================================
// Copyright (c) 1995 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;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import org.eclipse.jetty.client.internal.TunnelRequest;
import org.eclipse.jetty.client.transport.HttpConversation;
import org.eclipse.jetty.client.transport.HttpDestination;
import org.eclipse.jetty.client.transport.HttpRequest;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.io.ClientConnectionFactory;
import org.eclipse.jetty.io.ClientConnector;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.io.Transport;
import org.eclipse.jetty.util.Attachable;
import org.eclipse.jetty.util.Promise;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 

Client-side proxy configuration for HTTP proxying, as specified by * RFC 9110.

*

By default the communication between client and proxy happens using * the HTTP/1.1 protocol, but it may be configured to use * also other HTTP protocol versions, such as HTTP/2.

*/ public class HttpProxy extends ProxyConfiguration.Proxy { private static final Logger LOG = LoggerFactory.getLogger(HttpProxy.class); /** *

Creates a new instance with the given HTTP proxy host and port.

* * @param host the HTTP proxy host name * @param port the HTTP proxy port */ public HttpProxy(String host, int port) { this(new Origin.Address(host, port), false); } /** *

Creates a new instance with the given HTTP proxy address.

*

When {@code secure=true} the communication between the client and the * proxy will be encrypted (using this proxy {@link #getSslContextFactory()} * which typically defaults to that of {@link HttpClient}.

* * @param address the HTTP proxy address (host and port) * @param secure whether the communication between the client and the HTTP proxy should be secure */ public HttpProxy(Origin.Address address, boolean secure) { this(address, secure, new Origin.Protocol(List.of("http/1.1"), false)); } /** *

Creates a new instance with the given HTTP proxy address and protocol.

* * @param address the HTTP proxy address (host and port) * @param secure whether the communication between the client and the HTTP proxy should be secure * @param protocol the protocol to use to communicate with the HTTP proxy */ public HttpProxy(Origin.Address address, boolean secure, Origin.Protocol protocol) { this(new Origin(secure ? "https" : "http", address, null, protocol, Transport.TCP_IP), null); } /** *

Creates a new instance with the given HTTP proxy address and TLS configuration.

*

The {@link SslContextFactory} could have a different configuration from the * one configured in {@link HttpClient}, and it is used to communicate with the HTTP * proxy only (not to communicate with the servers).

* * @param address the HTTP proxy address (host and port) * @param sslContextFactory the {@link SslContextFactory.Client} to use to communicate with the HTTP proxy */ public HttpProxy(Origin.Address address, SslContextFactory.Client sslContextFactory) { this(address, sslContextFactory, new Origin.Protocol(List.of("http/1.1"), false)); } /** *

Creates a new instance with the given HTTP proxy address, TLS configuration and protocol.

*

The {@link SslContextFactory} could have a different configuration from the * one configured in {@link HttpClient} and it is used to communicate with the HTTP * proxy only (not to communicate with the servers).

* * @param address the HTTP proxy address (host and port) * @param sslContextFactory the {@link SslContextFactory.Client} to use to communicate with the HTTP proxy * @param protocol the protocol to use to communicate with the HTTP proxy */ public HttpProxy(Origin.Address address, SslContextFactory.Client sslContextFactory, Origin.Protocol protocol) { this(new Origin(sslContextFactory == null ? "http" : "https", address, null, protocol, Transport.TCP_IP), sslContextFactory); } /** *

Creates a new instance with the given HTTP proxy {@link Origin} and TLS configuration.

*

The {@link SslContextFactory} could have a different configuration from the * one configured in {@link HttpClient} and it is used to communicate with the HTTP * proxy only (not to communicate with the servers).

* * @param origin the HTTP proxy {@link Origin} information * @param sslContextFactory the {@link SslContextFactory.Client} to use to communicate with the HTTP proxy */ public HttpProxy(Origin origin, SslContextFactory.Client sslContextFactory) { super(origin, sslContextFactory); } @Override public ClientConnectionFactory newClientConnectionFactory(ClientConnectionFactory connectionFactory) { return new HttpProxyClientConnectionFactory(connectionFactory); } @Override public URI getURI() { return URI.create(getOrigin().asString()); } public boolean requiresTunnel(Origin serverOrigin) { if (HttpScheme.isSecure(serverOrigin.getScheme())) return true; Origin.Protocol serverProtocol = serverOrigin.getProtocol(); if (serverProtocol == null) return true; List serverProtocols = serverProtocol.getProtocols(); return getProtocol().getProtocols().stream().noneMatch(p -> protocolMatches(p, serverProtocols)); } private boolean protocolMatches(String protocol, List protocols) { return protocols.stream().anyMatch(p -> protocol.equalsIgnoreCase(p) || (isHTTP2(p) && isHTTP2(protocol))); } private boolean isHTTP2(String protocol) { return "h2".equalsIgnoreCase(protocol) || "h2c".equalsIgnoreCase(protocol); } private class HttpProxyClientConnectionFactory implements ClientConnectionFactory { private final ClientConnectionFactory connectionFactory; private HttpProxyClientConnectionFactory(ClientConnectionFactory connectionFactory) { this.connectionFactory = connectionFactory; } @Override public org.eclipse.jetty.io.Connection newConnection(EndPoint endPoint, Map context) throws IOException { HttpDestination destination = (HttpDestination)context.get(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY); if (requiresTunnel(destination.getOrigin())) return newProxyConnection(endPoint, context); else return connectionFactory.newConnection(endPoint, context); } private org.eclipse.jetty.io.Connection newProxyConnection(EndPoint endPoint, Map context) throws IOException { // Replace the destination with the proxy destination. HttpDestination destination = (HttpDestination)context.get(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY); HttpClient client = destination.getHttpClient(); Destination proxyDestination = client.resolveDestination(getOrigin()); context.put(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY, proxyDestination); // Replace the promise with the proxy promise that creates the tunnel to the server. @SuppressWarnings("unchecked") Promise promise = (Promise)context.get(HttpClientTransport.HTTP_CONNECTION_PROMISE_CONTEXT_KEY); CreateTunnelPromise tunnelPromise = new CreateTunnelPromise(connectionFactory, endPoint, destination, promise, context); context.put(HttpClientTransport.HTTP_CONNECTION_PROMISE_CONTEXT_KEY, tunnelPromise); return connectionFactory.newConnection(endPoint, context); } } /** *

Creates a tunnel using HTTP CONNECT.

*

It is implemented as a promise because it needs to establish the * tunnel after the TCP connection is succeeded, and needs to notify * the nested promise when the tunnel is established (or failed).

*/ private static class CreateTunnelPromise implements Promise { private final ClientConnectionFactory connectionFactory; private final EndPoint endPoint; private final HttpDestination destination; private final Promise promise; private final Map context; private CreateTunnelPromise(ClientConnectionFactory connectionFactory, EndPoint endPoint, HttpDestination destination, Promise promise, Map context) { this.connectionFactory = connectionFactory; this.endPoint = endPoint; this.destination = destination; this.promise = promise; this.context = context; } @Override public void succeeded(Connection connection) { // Replace the destination back with the original. context.put(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY, destination); // Replace the promise back with the original. context.put(HttpClientTransport.HTTP_CONNECTION_PROMISE_CONTEXT_KEY, promise); tunnel(connection); } @Override public void failed(Throwable x) { tunnelFailed(endPoint, x); } private void tunnel(Connection connection) { String target = destination.getOrigin().getAddress().asString(); HttpClient httpClient = destination.getHttpClient(); long connectTimeout = httpClient.getConnectTimeout(); Request connect = new TunnelRequest(httpClient, destination.getProxy().getURI()) .path(target) .headers(headers -> headers.put(HttpHeader.HOST, target)) // Use the connect timeout as a total timeout, // since this request is to "connect" to the server. .timeout(connectTimeout, TimeUnit.MILLISECONDS); Destination proxyDestination = httpClient.resolveDestination(destination.getProxy().getOrigin()); connect.attribute(Connection.class.getName(), new ProxyConnection(proxyDestination, connection, promise)); connection.send(connect, new TunnelListener(connect)); } private void tunnelSucceeded(EndPoint endPoint) { try { HttpDestination destination = (HttpDestination)context.get(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY); ClientConnectionFactory factory = connectionFactory; if (destination.isSecure()) { // Don't want to do DNS resolution here. InetSocketAddress address = InetSocketAddress.createUnresolved(destination.getHost(), destination.getPort()); context.put(ClientConnector.REMOTE_SOCKET_ADDRESS_CONTEXT_KEY, address); factory = destination.getHttpClient().newSslClientConnectionFactory(null, factory); } var oldConnection = endPoint.getConnection(); var newConnection = factory.newConnection(endPoint, context); if (LOG.isDebugEnabled()) LOG.debug("HTTP tunnel established: {} over {}", oldConnection, newConnection); endPoint.upgrade(newConnection); } catch (Throwable x) { tunnelFailed(endPoint, x); } } private void tunnelFailed(EndPoint endPoint, Throwable failure) { endPoint.close(failure); promise.failed(failure); } private class TunnelListener implements Response.Listener { private final HttpConversation conversation; private TunnelListener(Request request) { this.conversation = ((HttpRequest)request).getConversation(); } @Override public void onHeaders(Response response) { // The EndPoint may have changed during the conversation, get the latest. EndPoint endPoint = (EndPoint)conversation.getAttribute(EndPoint.class.getName()); if (response.getStatus() == HttpStatus.OK_200) { tunnelSucceeded(endPoint); } else { HttpResponseException failure = new HttpResponseException("Unexpected " + response + " for " + response.getRequest(), response); tunnelFailed(endPoint, failure); } } @Override public void onComplete(Result result) { if (result.isFailed()) tunnelFailed(endPoint, result.getFailure()); } } } private static class ProxyConnection implements Connection, Attachable { private final Destination destination; private final Connection connection; private final Promise promise; private Object attachment; private ProxyConnection(Destination destination, Connection connection, Promise promise) { this.destination = destination; this.connection = connection; this.promise = promise; } @Override public SocketAddress getLocalSocketAddress() { return connection.getLocalSocketAddress(); } @Override public SocketAddress getRemoteSocketAddress() { return connection.getRemoteSocketAddress(); } @Override public EndPoint.SslSessionData getSslSessionData() { return connection.getSslSessionData(); } @Override public void send(Request request, Response.CompleteListener listener) { if (connection.isClosed()) { destination.newConnection(new TunnelPromise(request, listener, promise)); } else { connection.send(request, listener); } } @Override public void close() { connection.close(); } @Override public boolean isClosed() { return connection.isClosed(); } @Override public void setAttachment(Object obj) { this.attachment = obj; } @Override public Object getAttachment() { return attachment; } } private static class TunnelPromise implements Promise { private final Request request; private final Response.CompleteListener listener; private final Promise promise; private TunnelPromise(Request request, Response.CompleteListener listener, Promise promise) { this.request = request; this.listener = listener; this.promise = promise; } @Override public void succeeded(Connection connection) { connection.send(request, listener); } @Override public void failed(Throwable x) { promise.failed(x); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy