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.StandardAuthScheme;
40  import org.apache.hc.client5.http.auth.AuthScope;
41  import org.apache.hc.client5.http.auth.ChallengeType;
42  import org.apache.hc.client5.http.auth.Credentials;
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.TunnelRefusedException;
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.auth.KerberosSchemeFactory;
51  import org.apache.hc.client5.http.impl.auth.NTLMSchemeFactory;
52  import org.apache.hc.client5.http.impl.auth.SPNegoSchemeFactory;
53  import org.apache.hc.client5.http.impl.io.ManagedHttpClientConnectionFactory;
54  import org.apache.hc.client5.http.io.ManagedHttpClientConnection;
55  import org.apache.hc.client5.http.protocol.HttpClientContext;
56  import org.apache.hc.client5.http.protocol.RequestClientConnControl;
57  import org.apache.hc.core5.http.ClassicHttpRequest;
58  import org.apache.hc.core5.http.ClassicHttpResponse;
59  import org.apache.hc.core5.http.ConnectionReuseStrategy;
60  import org.apache.hc.core5.http.HttpEntity;
61  import org.apache.hc.core5.http.HttpException;
62  import org.apache.hc.core5.http.HttpHeaders;
63  import org.apache.hc.core5.http.HttpHost;
64  import org.apache.hc.core5.http.config.CharCodingConfig;
65  import org.apache.hc.core5.http.config.Http1Config;
66  import org.apache.hc.core5.http.config.Lookup;
67  import org.apache.hc.core5.http.config.RegistryBuilder;
68  import org.apache.hc.core5.http.impl.DefaultConnectionReuseStrategy;
69  import org.apache.hc.core5.http.impl.io.HttpRequestExecutor;
70  import org.apache.hc.core5.http.io.HttpConnectionFactory;
71  import org.apache.hc.core5.http.io.entity.EntityUtils;
72  import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
73  import org.apache.hc.core5.http.message.StatusLine;
74  import org.apache.hc.core5.http.protocol.BasicHttpContext;
75  import org.apache.hc.core5.http.protocol.DefaultHttpProcessor;
76  import org.apache.hc.core5.http.protocol.HttpContext;
77  import org.apache.hc.core5.http.protocol.HttpCoreContext;
78  import org.apache.hc.core5.http.protocol.HttpProcessor;
79  import org.apache.hc.core5.http.protocol.RequestTargetHost;
80  import org.apache.hc.core5.http.protocol.RequestUserAgent;
81  import org.apache.hc.core5.util.Args;
82  
83  /**
84   * ProxyClient can be used to establish a tunnel via an HTTP/1.1 proxy.
85   */
86  public class ProxyClient {
87  
88      private final HttpConnectionFactory<ManagedHttpClientConnection> connFactory;
89      private final RequestConfig requestConfig;
90      private final HttpProcessor httpProcessor;
91      private final HttpRequestExecutor requestExec;
92      private final AuthenticationStrategy proxyAuthStrategy;
93      private final HttpAuthenticator authenticator;
94      private final AuthExchange proxyAuthExchange;
95      private final Lookup<AuthSchemeFactory> authSchemeRegistry;
96      private final ConnectionReuseStrategy reuseStrategy;
97  
98      /**
99       * @since 5.0
100      */
101     public ProxyClient(
102             final HttpConnectionFactory<ManagedHttpClientConnection> connFactory,
103             final Http1Config h1Config,
104             final CharCodingConfig charCodingConfig,
105             final RequestConfig requestConfig) {
106         super();
107         this.connFactory = connFactory != null ? connFactory : new ManagedHttpClientConnectionFactory(h1Config, charCodingConfig, null, null);
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                 .register(StandardAuthScheme.NTLM, NTLMSchemeFactory.INSTANCE)
119                 .register(StandardAuthScheme.SPNEGO, SPNegoSchemeFactory.DEFAULT)
120                 .register(StandardAuthScheme.KERBEROS, KerberosSchemeFactory.DEFAULT)
121                 .build();
122         this.reuseStrategy = new DefaultConnectionReuseStrategy();
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/http/HttpRoute.html#HttpRoute">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("CONNECT", host.toHostString());
157 
158         final BasicCredentialsProvideredentialsProvider.html#BasicCredentialsProvider">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 }