View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2014 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.api.Result;
30  import org.eclipse.jetty.client.http.HttpConnectionOverHTTP;
31  import org.eclipse.jetty.http.HttpHeader;
32  import org.eclipse.jetty.http.HttpMethod;
33  import org.eclipse.jetty.http.HttpScheme;
34  import org.eclipse.jetty.io.ClientConnectionFactory;
35  import org.eclipse.jetty.io.EndPoint;
36  import org.eclipse.jetty.io.ssl.SslClientConnectionFactory;
37  import org.eclipse.jetty.util.Promise;
38  import org.eclipse.jetty.util.log.Log;
39  import org.eclipse.jetty.util.log.Logger;
40  import org.eclipse.jetty.util.ssl.SslContextFactory;
41  
42  public class HttpProxy extends ProxyConfiguration.Proxy
43  {
44      public HttpProxy(String host, int port)
45      {
46          this(new Origin.Address(host, port), false);
47      }
48  
49      public HttpProxy(Origin.Address address, boolean secure)
50      {
51          super(address, secure);
52      }
53  
54      @Override
55      public ClientConnectionFactory newClientConnectionFactory(ClientConnectionFactory connectionFactory)
56      {
57          return new HttpProxyClientConnectionFactory(connectionFactory);
58      }
59  
60      @Override
61      public URI getURI()
62      {
63          String scheme = isSecure() ? HttpScheme.HTTPS.asString() : HttpScheme.HTTP.asString();
64          return URI.create(new Origin(scheme, getAddress()).asString());
65      }
66  
67      public static class HttpProxyClientConnectionFactory implements ClientConnectionFactory
68      {
69          private static final Logger LOG = Log.getLogger(HttpProxyClientConnectionFactory.class);
70          private final ClientConnectionFactory connectionFactory;
71  
72          public HttpProxyClientConnectionFactory(ClientConnectionFactory connectionFactory)
73          {
74              this.connectionFactory = connectionFactory;
75          }
76  
77          @Override
78          public org.eclipse.jetty.io.Connection newConnection(EndPoint endPoint, Map<String, Object> context) throws IOException
79          {
80              @SuppressWarnings("unchecked")
81              Promise<Connection> promise = (Promise<Connection>)context.get(HttpClientTransport.HTTP_CONNECTION_PROMISE_CONTEXT_KEY);
82              final ProxyPromise proxyPromise = new ProxyPromise(endPoint, promise, context);
83              // Replace the promise with the proxy one
84              context.put(HttpClientTransport.HTTP_CONNECTION_PROMISE_CONTEXT_KEY, proxyPromise);
85              return connectionFactory.newConnection(endPoint, context);
86          }
87  
88          /**
89           * Decides whether to establish a proxy tunnel using HTTP CONNECT.
90           * It is implemented as a promise because it needs to establish the
91           * tunnel after the TCP connection is succeeded, and needs to notify
92           * the nested promise when the tunnel is established (or failed).
93           */
94          private class ProxyPromise implements Promise<Connection>
95          {
96              private final EndPoint endPoint;
97              private final Promise<Connection> promise;
98              private final Map<String, Object> context;
99  
100             private ProxyPromise(EndPoint endPoint, Promise<Connection> promise, Map<String, Object> context)
101             {
102                 this.endPoint = endPoint;
103                 this.promise = promise;
104                 this.context = context;
105             }
106 
107             @Override
108             public void succeeded(Connection connection)
109             {
110                 HttpDestination destination = (HttpDestination)context.get(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY);
111                 if (HttpScheme.HTTPS.is(destination.getScheme()))
112                 {
113                     SslContextFactory sslContextFactory = destination.getHttpClient().getSslContextFactory();
114                     if (sslContextFactory != null)
115                     {
116                         tunnel(destination, connection);
117                     }
118                     else
119                     {
120                         String message = String.format("Cannot perform requests over SSL, no %s in %s",
121                                 SslContextFactory.class.getSimpleName(), HttpClient.class.getSimpleName());
122                         promise.failed(new IllegalStateException(message));
123                     }
124                 }
125                 else
126                 {
127                     promise.succeeded(connection);
128                 }
129             }
130 
131             @Override
132             public void failed(Throwable x)
133             {
134                 promise.failed(x);
135             }
136 
137             private void tunnel(HttpDestination destination, final Connection connection)
138             {
139                 String target = destination.getOrigin().getAddress().asString();
140                 Origin.Address proxyAddress = destination.getConnectAddress();
141                 HttpClient httpClient = destination.getHttpClient();
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                         .timeout(httpClient.getConnectTimeout(), TimeUnit.MILLISECONDS);
148 
149                 connection.send(connect, new Response.CompleteListener()
150                 {
151                     @Override
152                     public void onComplete(Result result)
153                     {
154                         if (result.isFailed())
155                         {
156                             tunnelFailed(result.getFailure());
157                         }
158                         else
159                         {
160                             Response response = result.getResponse();
161                             if (response.getStatus() == 200)
162                             {
163                                 tunnelSucceeded();
164                             }
165                             else
166                             {
167                                 tunnelFailed(new HttpResponseException("Received " + response + " for " + result.getRequest(), response));
168                             }
169                         }
170                     }
171                 });
172             }
173 
174             private void tunnelSucceeded()
175             {
176                 try
177                 {
178                     // Replace the promise back with the original
179                     context.put(HttpClientTransport.HTTP_CONNECTION_PROMISE_CONTEXT_KEY, promise);
180                     HttpDestination destination = (HttpDestination)context.get(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY);
181                     HttpClient client = destination.getHttpClient();
182                     ClientConnectionFactory sslConnectionFactory = new SslClientConnectionFactory(client.getSslContextFactory(), client.getByteBufferPool(), client.getExecutor(), connectionFactory);
183                     HttpConnectionOverHTTP oldConnection = (HttpConnectionOverHTTP)endPoint.getConnection();
184                     org.eclipse.jetty.io.Connection newConnection = sslConnectionFactory.newConnection(endPoint, context);
185                     Helper.replaceConnection(oldConnection, newConnection);
186                     // Avoid setting fill interest in the old Connection,
187                     // without closing the underlying EndPoint.
188                     oldConnection.softClose();
189                     if (LOG.isDebugEnabled())
190                         LOG.debug("HTTP tunnel established: {} over {}", oldConnection, newConnection);
191                 }
192                 catch (Throwable x)
193                 {
194                     tunnelFailed(x);
195                 }
196             }
197 
198             private void tunnelFailed(Throwable failure)
199             {
200                 endPoint.close();
201                 failed(failure);
202             }
203         }
204     }
205 }