View Javadoc
1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
25   *
26   */
27  
28  package org.apache.hc.client5.http.impl.classic;
29  
30  import java.io.IOException;
31  import java.net.Socket;
32  
33  import org.apache.hc.client5.http.AuthenticationStrategy;
34  import org.apache.hc.client5.http.HttpRoute;
35  import org.apache.hc.client5.http.RouteInfo.LayerType;
36  import org.apache.hc.client5.http.RouteInfo.TunnelType;
37  import org.apache.hc.client5.http.auth.AuthExchange;
38  import org.apache.hc.client5.http.auth.AuthSchemeFactory;
39  import org.apache.hc.client5.http.auth.AuthScope;
40  import org.apache.hc.client5.http.auth.ChallengeType;
41  import org.apache.hc.client5.http.auth.Credentials;
42  import org.apache.hc.client5.http.auth.StandardAuthScheme;
43  import org.apache.hc.client5.http.config.RequestConfig;
44  import org.apache.hc.client5.http.impl.DefaultAuthenticationStrategy;
45  import org.apache.hc.client5.http.impl.DefaultClientConnectionReuseStrategy;
46  import org.apache.hc.client5.http.impl.TunnelRefusedException;
47  import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
48  import org.apache.hc.client5.http.impl.auth.BasicSchemeFactory;
49  import org.apache.hc.client5.http.impl.auth.DigestSchemeFactory;
50  import org.apache.hc.client5.http.impl.auth.HttpAuthenticator;
51  import org.apache.hc.client5.http.impl.io.ManagedHttpClientConnectionFactory;
52  import org.apache.hc.client5.http.io.ManagedHttpClientConnection;
53  import org.apache.hc.client5.http.protocol.HttpClientContext;
54  import org.apache.hc.client5.http.protocol.RequestClientConnControl;
55  import org.apache.hc.core5.http.ClassicHttpRequest;
56  import org.apache.hc.core5.http.ClassicHttpResponse;
57  import org.apache.hc.core5.http.ConnectionReuseStrategy;
58  import org.apache.hc.core5.http.HttpEntity;
59  import org.apache.hc.core5.http.HttpException;
60  import org.apache.hc.core5.http.HttpHeaders;
61  import org.apache.hc.core5.http.HttpHost;
62  import org.apache.hc.core5.http.Method;
63  import org.apache.hc.core5.http.config.CharCodingConfig;
64  import org.apache.hc.core5.http.config.Http1Config;
65  import org.apache.hc.core5.http.config.Lookup;
66  import org.apache.hc.core5.http.config.RegistryBuilder;
67  import org.apache.hc.core5.http.impl.io.HttpRequestExecutor;
68  import org.apache.hc.core5.http.io.HttpConnectionFactory;
69  import org.apache.hc.core5.http.io.entity.EntityUtils;
70  import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
71  import org.apache.hc.core5.http.message.StatusLine;
72  import org.apache.hc.core5.http.protocol.BasicHttpContext;
73  import org.apache.hc.core5.http.protocol.DefaultHttpProcessor;
74  import org.apache.hc.core5.http.protocol.HttpContext;
75  import org.apache.hc.core5.http.protocol.HttpCoreContext;
76  import org.apache.hc.core5.http.protocol.HttpProcessor;
77  import org.apache.hc.core5.http.protocol.RequestTargetHost;
78  import org.apache.hc.core5.http.protocol.RequestUserAgent;
79  import org.apache.hc.core5.util.Args;
80  
81  /**
82   * ProxyClient can be used to establish a tunnel via an HTTP/1.1 proxy.
83   */
84  public class ProxyClient {
85  
86      private final HttpConnectionFactory<ManagedHttpClientConnection> connFactory;
87      private final RequestConfig requestConfig;
88      private final HttpProcessor httpProcessor;
89      private final HttpRequestExecutor requestExec;
90      private final AuthenticationStrategy proxyAuthStrategy;
91      private final HttpAuthenticator authenticator;
92      private final AuthExchange proxyAuthExchange;
93      private final Lookup<AuthSchemeFactory> authSchemeRegistry;
94      private final ConnectionReuseStrategy reuseStrategy;
95  
96      /**
97       * @since 5.0
98       */
99      public ProxyClient(
100             final HttpConnectionFactory<ManagedHttpClientConnection> connFactory,
101             final Http1Config h1Config,
102             final CharCodingConfig charCodingConfig,
103             final RequestConfig requestConfig) {
104         super();
105         this.connFactory = connFactory != null
106                 ? connFactory
107                 : ManagedHttpClientConnectionFactory.builder()
108                 .http1Config(h1Config)
109                 .charCodingConfig(charCodingConfig)
110                 .build();
111         this.requestConfig = requestConfig != null ? requestConfig : RequestConfig.DEFAULT;
112         this.httpProcessor = new DefaultHttpProcessor(
113                 new RequestTargetHost(), new RequestClientConnControl(), new RequestUserAgent());
114         this.requestExec = new HttpRequestExecutor();
115         this.proxyAuthStrategy = new DefaultAuthenticationStrategy();
116         this.authenticator = new HttpAuthenticator();
117         this.proxyAuthExchange = new AuthExchange();
118         this.authSchemeRegistry = RegistryBuilder.<AuthSchemeFactory>create()
119                 .register(StandardAuthScheme.BASIC, BasicSchemeFactory.INSTANCE)
120                 .register(StandardAuthScheme.DIGEST, DigestSchemeFactory.INSTANCE)
121                 .build();
122         this.reuseStrategy = DefaultClientConnectionReuseStrategy.INSTANCE;
123     }
124 
125     /**
126      * @since 4.3
127      */
128     public ProxyClient(final RequestConfig requestConfig) {
129         this(null, null, null, requestConfig);
130     }
131 
132     public ProxyClient() {
133         this(null, null, null, null);
134     }
135 
136     public Socket tunnel(
137             final HttpHost proxy,
138             final HttpHost target,
139             final Credentials credentials) throws IOException, HttpException {
140         Args.notNull(proxy, "Proxy host");
141         Args.notNull(target, "Target host");
142         Args.notNull(credentials, "Credentials");
143         HttpHost host = target;
144         if (host.getPort() <= 0) {
145             host = new HttpHost(host.getSchemeName(), host.getHostName(), 80);
146         }
147         final HttpRoute route = new HttpRoute(
148                 host,
149                 null,
150                 proxy, false, TunnelType.TUNNELLED, LayerType.PLAIN);
151 
152         final ManagedHttpClientConnection conn = this.connFactory.createConnection(null);
153         final HttpContext context = new BasicHttpContext();
154         ClassicHttpResponse response;
155 
156         final ClassicHttpRequest connect = new BasicClassicHttpRequest(Method.CONNECT, proxy, host.toHostString());
157 
158         final BasicCredentialsProvider credsProvider = new BasicCredentialsProvider();
159         credsProvider.setCredentials(new AuthScope(proxy), credentials);
160 
161         // Populate the execution context
162         context.setAttribute(HttpCoreContext.HTTP_REQUEST, connect);
163         context.setAttribute(HttpClientContext.HTTP_ROUTE, route);
164         context.setAttribute(HttpClientContext.CREDS_PROVIDER, credsProvider);
165         context.setAttribute(HttpClientContext.AUTHSCHEME_REGISTRY, this.authSchemeRegistry);
166         context.setAttribute(HttpClientContext.REQUEST_CONFIG, this.requestConfig);
167 
168         this.requestExec.preProcess(connect, this.httpProcessor, context);
169 
170         for (;;) {
171             if (!conn.isOpen()) {
172                 final Socket socket = new Socket(proxy.getHostName(), proxy.getPort());
173                 conn.bind(socket);
174             }
175 
176             this.authenticator.addAuthResponse(proxy, ChallengeType.PROXY, connect, this.proxyAuthExchange, context);
177 
178             response = this.requestExec.execute(connect, conn, context);
179 
180             final int status = response.getCode();
181             if (status < 200) {
182                 throw new HttpException("Unexpected response to CONNECT request: " + response);
183             }
184             if (this.authenticator.isChallenged(proxy, ChallengeType.PROXY, response, this.proxyAuthExchange, context)) {
185                 if (this.authenticator.updateAuthState(proxy, ChallengeType.PROXY, response,
186                         this.proxyAuthStrategy, this.proxyAuthExchange, context)) {
187                     // Retry request
188                     if (this.reuseStrategy.keepAlive(connect, response, context)) {
189                         // Consume response content
190                         final HttpEntity entity = response.getEntity();
191                         EntityUtils.consume(entity);
192                     } else {
193                         conn.close();
194                     }
195                     // discard previous auth header
196                     connect.removeHeaders(HttpHeaders.PROXY_AUTHORIZATION);
197                 } else {
198                     break;
199                 }
200             } else {
201                 break;
202             }
203         }
204 
205         final int status = response.getCode();
206 
207         if (status > 299) {
208 
209             // Buffer response content
210             final HttpEntity entity = response.getEntity();
211             final String responseMessage = entity != null ? EntityUtils.toString(entity) : null;
212             conn.close();
213             throw new TunnelRefusedException("CONNECT refused by proxy: " + new StatusLine(response), responseMessage);
214         }
215         return conn.getSocket();
216     }
217 
218 }