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