View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
16  //  ========================================================================
17  //
18  
19  package org.eclipse.jetty.client;
20  
21  import java.io.IOException;
22  import java.net.URI;
23  import java.util.Map;
24  import java.util.concurrent.TimeUnit;
25  
26  import org.eclipse.jetty.client.api.Connection;
27  import org.eclipse.jetty.client.api.Request;
28  import org.eclipse.jetty.client.api.Response;
29  import org.eclipse.jetty.client.http.HttpConnectionOverHTTP;
30  import org.eclipse.jetty.http.HttpHeader;
31  import org.eclipse.jetty.http.HttpMethod;
32  import org.eclipse.jetty.http.HttpScheme;
33  import org.eclipse.jetty.io.ClientConnectionFactory;
34  import org.eclipse.jetty.io.EndPoint;
35  import org.eclipse.jetty.io.ssl.SslClientConnectionFactory;
36  import org.eclipse.jetty.util.Promise;
37  import org.eclipse.jetty.util.log.Log;
38  import org.eclipse.jetty.util.log.Logger;
39  import org.eclipse.jetty.util.ssl.SslContextFactory;
40  
41  public class HttpProxy extends ProxyConfiguration.Proxy
42  {
43      public HttpProxy(String host, int port)
44      {
45          this(new Origin.Address(host, port), false);
46      }
47  
48      public HttpProxy(Origin.Address address, boolean secure)
49      {
50          super(address, secure);
51      }
52  
53      @Override
54      public ClientConnectionFactory newClientConnectionFactory(ClientConnectionFactory connectionFactory)
55      {
56          return new HttpProxyClientConnectionFactory(connectionFactory);
57      }
58  
59      @Override
60      public URI getURI()
61      {
62          String scheme = isSecure() ? HttpScheme.HTTPS.asString() : HttpScheme.HTTP.asString();
63          return URI.create(new Origin(scheme, getAddress()).asString());
64      }
65  
66      public static class HttpProxyClientConnectionFactory implements ClientConnectionFactory
67      {
68          private static final Logger LOG = Log.getLogger(HttpProxyClientConnectionFactory.class);
69          private final ClientConnectionFactory connectionFactory;
70  
71          public HttpProxyClientConnectionFactory(ClientConnectionFactory connectionFactory)
72          {
73              this.connectionFactory = connectionFactory;
74          }
75  
76          @Override
77          public org.eclipse.jetty.io.Connection newConnection(EndPoint endPoint, Map<String, Object> context) throws IOException
78          {
79              @SuppressWarnings("unchecked")
80              Promise<Connection> promise = (Promise<Connection>)context.get(HttpClientTransport.HTTP_CONNECTION_PROMISE_CONTEXT_KEY);
81              final ProxyPromise proxyPromise = new ProxyPromise(endPoint, promise, context);
82              // Replace the promise with the proxy one
83              context.put(HttpClientTransport.HTTP_CONNECTION_PROMISE_CONTEXT_KEY, proxyPromise);
84              return connectionFactory.newConnection(endPoint, context);
85          }
86  
87          /**
88           * Decides whether to establish a proxy tunnel using HTTP CONNECT.
89           * It is implemented as a promise because it needs to establish the
90           * tunnel after the TCP connection is succeeded, and needs to notify
91           * the nested promise when the tunnel is established (or failed).
92           */
93          private class ProxyPromise implements Promise<Connection>
94          {
95              private final EndPoint endPoint;
96              private final Promise<Connection> promise;
97              private final Map<String, Object> context;
98  
99              private ProxyPromise(EndPoint endPoint, Promise<Connection> promise, Map<String, Object> context)
100             {
101                 this.endPoint = endPoint;
102                 this.promise = promise;
103                 this.context = context;
104             }
105 
106             @Override
107             public void succeeded(Connection connection)
108             {
109                 HttpDestination destination = (HttpDestination)context.get(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY);
110                 if (HttpScheme.HTTPS.is(destination.getScheme()))
111                 {
112                     SslContextFactory sslContextFactory = destination.getHttpClient().getSslContextFactory();
113                     if (sslContextFactory != null)
114                     {
115                         tunnel(destination, connection);
116                     }
117                     else
118                     {
119                         String message = String.format("Cannot perform requests over SSL, no %s in %s",
120                                 SslContextFactory.class.getSimpleName(), HttpClient.class.getSimpleName());
121                         tunnelFailed(new IllegalStateException(message));
122                     }
123                 }
124                 else
125                 {
126                     promise.succeeded(connection);
127                 }
128             }
129 
130             @Override
131             public void failed(Throwable x)
132             {
133                 tunnelFailed(x);
134             }
135 
136             private void tunnel(HttpDestination destination, final Connection connection)
137             {
138                 String target = destination.getOrigin().getAddress().asString();
139                 Origin.Address proxyAddress = destination.getConnectAddress();
140                 HttpClient httpClient = destination.getHttpClient();
141                 long connectTimeout = httpClient.getConnectTimeout();
142                 Request connect = httpClient.newRequest(proxyAddress.getHost(), proxyAddress.getPort())
143                         .scheme(HttpScheme.HTTP.asString())
144                         .method(HttpMethod.CONNECT)
145                         .path(target)
146                         .header(HttpHeader.HOST, target)
147                         .idleTimeout(2 * connectTimeout, TimeUnit.MILLISECONDS)
148                         .timeout(connectTimeout, TimeUnit.MILLISECONDS);
149 
150                 connection.send(connect, result ->
151                 {
152                     if (result.isFailed())
153                     {
154                         tunnelFailed(result.getFailure());
155                     }
156                     else
157                     {
158                         Response response = result.getResponse();
159                         if (response.getStatus() == 200)
160                         {
161                             tunnelSucceeded();
162                         }
163                         else
164                         {
165                             tunnelFailed(new HttpResponseException("Received " + response + " for " + result.getRequest(), response));
166                         }
167                     }
168                 });
169             }
170 
171             private void tunnelSucceeded()
172             {
173                 try
174                 {
175                     // Replace the promise back with the original
176                     context.put(HttpClientTransport.HTTP_CONNECTION_PROMISE_CONTEXT_KEY, promise);
177                     HttpDestination destination = (HttpDestination)context.get(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY);
178                     HttpClient client = destination.getHttpClient();
179                     ClientConnectionFactory sslConnectionFactory = new SslClientConnectionFactory(client.getSslContextFactory(), client.getByteBufferPool(), client.getExecutor(), connectionFactory);
180                     HttpConnectionOverHTTP oldConnection = (HttpConnectionOverHTTP)endPoint.getConnection();
181                     org.eclipse.jetty.io.Connection newConnection = sslConnectionFactory.newConnection(endPoint, context);
182                     endPoint.upgrade(newConnection);
183                     if (LOG.isDebugEnabled())
184                         LOG.debug("HTTP tunnel established: {} over {}", oldConnection, newConnection);
185                 }
186                 catch (Throwable x)
187                 {
188                     tunnelFailed(x);
189                 }
190             }
191 
192             private void tunnelFailed(Throwable failure)
193             {
194                 endPoint.close();
195                 promise.failed(failure);
196             }
197         }
198     }
199 }