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.auth.BasicCredentialsProvider;
47  import org.apache.hc.client5.http.impl.auth.BasicSchemeFactory;
48  import org.apache.hc.client5.http.impl.auth.DigestSchemeFactory;
49  import org.apache.hc.client5.http.impl.auth.HttpAuthenticator;
50  import org.apache.hc.client5.http.impl.io.ManagedHttpClientConnectionFactory;
51  import org.apache.hc.client5.http.io.ManagedHttpClientConnection;
52  import org.apache.hc.client5.http.protocol.HttpClientContext;
53  import org.apache.hc.client5.http.protocol.RequestClientConnControl;
54  import org.apache.hc.core5.http.ClassicHttpRequest;
55  import org.apache.hc.core5.http.ClassicHttpResponse;
56  import org.apache.hc.core5.http.ConnectionReuseStrategy;
57  import org.apache.hc.core5.http.HttpEntity;
58  import org.apache.hc.core5.http.HttpException;
59  import org.apache.hc.core5.http.HttpHeaders;
60  import org.apache.hc.core5.http.HttpHost;
61  import org.apache.hc.core5.http.Method;
62  import org.apache.hc.core5.http.config.CharCodingConfig;
63  import org.apache.hc.core5.http.config.Http1Config;
64  import org.apache.hc.core5.http.config.Lookup;
65  import org.apache.hc.core5.http.config.RegistryBuilder;
66  import org.apache.hc.core5.http.impl.io.HttpRequestExecutor;
67  import org.apache.hc.core5.http.io.HttpConnectionFactory;
68  import org.apache.hc.core5.http.io.entity.EntityUtils;
69  import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
70  import org.apache.hc.core5.http.message.StatusLine;
71  import org.apache.hc.core5.http.protocol.DefaultHttpProcessor;
72  import org.apache.hc.core5.http.protocol.HttpProcessor;
73  import org.apache.hc.core5.http.protocol.RequestTargetHost;
74  import org.apache.hc.core5.http.protocol.RequestUserAgent;
75  import org.apache.hc.core5.util.Args;
76  
77  /**
78   * ProxyClient can be used to establish a tunnel via an HTTP/1.1 proxy.
79   */
80  public class ProxyClient {
81  
82      private final HttpConnectionFactory<ManagedHttpClientConnection> connFactory;
83      private final RequestConfig requestConfig;
84      private final HttpProcessor httpProcessor;
85      private final HttpRequestExecutor requestExec;
86      private final AuthenticationStrategy proxyAuthStrategy;
87      private final HttpAuthenticator authenticator;
88      private final AuthExchange proxyAuthExchange;
89      private final Lookup<AuthSchemeFactory> authSchemeRegistry;
90      private final ConnectionReuseStrategy reuseStrategy;
91  
92      /**
93       * @since 5.0
94       */
95      public ProxyClient(
96              final HttpConnectionFactory<ManagedHttpClientConnection> connFactory,
97              final Http1Config h1Config,
98              final CharCodingConfig charCodingConfig,
99              final RequestConfig requestConfig) {
100         super();
101         this.connFactory = connFactory != null
102                 ? connFactory
103                 : ManagedHttpClientConnectionFactory.builder()
104                 .http1Config(h1Config)
105                 .charCodingConfig(charCodingConfig)
106                 .build();
107         this.requestConfig = requestConfig != null ? requestConfig : RequestConfig.DEFAULT;
108         this.httpProcessor = new DefaultHttpProcessor(
109                 new RequestTargetHost(), new RequestClientConnControl(), new RequestUserAgent());
110         this.requestExec = new HttpRequestExecutor();
111         this.proxyAuthStrategy = new DefaultAuthenticationStrategy();
112         this.authenticator = new HttpAuthenticator();
113         this.proxyAuthExchange = new AuthExchange();
114         this.authSchemeRegistry = RegistryBuilder.<AuthSchemeFactory>create()
115                 .register(StandardAuthScheme.BASIC, BasicSchemeFactory.INSTANCE)
116                 .register(StandardAuthScheme.DIGEST, DigestSchemeFactory.INSTANCE)
117                 .build();
118         this.reuseStrategy = DefaultClientConnectionReuseStrategy.INSTANCE;
119     }
120 
121     /**
122      * @since 4.3
123      */
124     public ProxyClient(final RequestConfig requestConfig) {
125         this(null, null, null, requestConfig);
126     }
127 
128     public ProxyClient() {
129         this(null, null, null, null);
130     }
131 
132     public Socket tunnel(
133             final HttpHost proxy,
134             final HttpHost target,
135             final Credentials credentials) throws IOException, HttpException {
136         Args.notNull(proxy, "Proxy host");
137         Args.notNull(target, "Target host");
138         Args.notNull(credentials, "Credentials");
139         Args.check(target.getPort() > 0, "A valid port number must be provided for the tunnel CONNECT request.");
140 
141         final HttpRoute route = new HttpRoute(
142                 target,
143                 null,
144                 proxy, false, TunnelType.TUNNELLED, LayerType.PLAIN);
145 
146         final ManagedHttpClientConnection conn = this.connFactory.createConnection(null);
147         final HttpClientContext context = HttpClientContext.create();
148         ClassicHttpResponse response;
149 
150         final ClassicHttpRequest connect = new BasicClassicHttpRequest(Method.CONNECT, proxy, target.toHostString());
151 
152         final BasicCredentialsProvider credsProvider = new BasicCredentialsProvider();
153         credsProvider.setCredentials(new AuthScope(proxy), credentials);
154 
155         // Populate the execution context
156         context.setRequest(connect);
157         context.setRoute(route);
158         context.setCredentialsProvider(credsProvider);
159         context.setAuthSchemeRegistry(this.authSchemeRegistry);
160         context.setRequestConfig(this.requestConfig);
161 
162         this.requestExec.preProcess(connect, this.httpProcessor, context);
163 
164         for (;;) {
165             if (!conn.isOpen()) {
166                 final Socket socket = new Socket(proxy.getHostName(), proxy.getPort());
167                 conn.bind(socket);
168             }
169 
170             this.authenticator.addAuthResponse(proxy, ChallengeType.PROXY, connect, this.proxyAuthExchange, context);
171 
172             response = this.requestExec.execute(connect, conn, context);
173 
174             final int status = response.getCode();
175             if (status < 200) {
176                 throw new HttpException("Unexpected response to CONNECT request: " + response);
177             }
178             if (this.authenticator.isChallenged(proxy, ChallengeType.PROXY, response, this.proxyAuthExchange, context)) {
179                 if (this.authenticator.updateAuthState(proxy, ChallengeType.PROXY, response,
180                         this.proxyAuthStrategy, this.proxyAuthExchange, context)) {
181                     // Retry request
182                     if (this.reuseStrategy.keepAlive(connect, response, context)) {
183                         // Consume response content
184                         final HttpEntity entity = response.getEntity();
185                         EntityUtils.consume(entity);
186                     } else {
187                         conn.close();
188                     }
189                     // discard previous auth header
190                     connect.removeHeaders(HttpHeaders.PROXY_AUTHORIZATION);
191                 } else {
192                     break;
193                 }
194             } else {
195                 break;
196             }
197         }
198 
199         final int status = response.getCode();
200 
201         if (status > 299) {
202             EntityUtils.consume(response.getEntity());
203             conn.close();
204             throw new HttpException("Tunnel refused: " + new StatusLine(response));
205         }
206         return conn.getSocket();
207     }
208 
209 }