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