xref: /JGit/org.eclipse.jgit.http.apache/src/org/eclipse/jgit/transport/http/apache/HttpClientConnection.java (revision 471ad49546eec8850dd7d00b8fc2b8b50e4a9446)
1 /*
2  * Copyright (C) 2013, 2020 Christian Halstrick <christian.halstrick@sap.com> and others
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Distribution License v. 1.0 which is available at
6  * https://www.eclipse.org/org/documents/edl-v10.php.
7  *
8  * SPDX-License-Identifier: BSD-3-Clause
9  */
10 package org.eclipse.jgit.transport.http.apache;
11 
12 import static org.eclipse.jgit.util.HttpSupport.METHOD_GET;
13 import static org.eclipse.jgit.util.HttpSupport.METHOD_HEAD;
14 import static org.eclipse.jgit.util.HttpSupport.METHOD_POST;
15 import static org.eclipse.jgit.util.HttpSupport.METHOD_PUT;
16 
17 import java.io.IOException;
18 import java.io.InputStream;
19 import java.io.OutputStream;
20 import java.net.InetSocketAddress;
21 import java.net.MalformedURLException;
22 import java.net.ProtocolException;
23 import java.net.Proxy;
24 import java.net.URL;
25 import java.security.KeyManagementException;
26 import java.security.NoSuchAlgorithmException;
27 import java.security.SecureRandom;
28 import java.util.Arrays;
29 import java.util.Collections;
30 import java.util.HashMap;
31 import java.util.LinkedList;
32 import java.util.List;
33 import java.util.Map;
34 import java.util.stream.Collectors;
35 
36 import javax.net.ssl.HostnameVerifier;
37 import javax.net.ssl.KeyManager;
38 import javax.net.ssl.SSLContext;
39 import javax.net.ssl.SSLSocket;
40 import javax.net.ssl.TrustManager;
41 
42 import org.apache.http.Header;
43 import org.apache.http.HeaderElement;
44 import org.apache.http.HttpEntity;
45 import org.apache.http.HttpEntityEnclosingRequest;
46 import org.apache.http.HttpHost;
47 import org.apache.http.HttpResponse;
48 import org.apache.http.client.ClientProtocolException;
49 import org.apache.http.client.HttpClient;
50 import org.apache.http.client.config.RequestConfig;
51 import org.apache.http.client.methods.HttpGet;
52 import org.apache.http.client.methods.HttpHead;
53 import org.apache.http.client.methods.HttpPost;
54 import org.apache.http.client.methods.HttpPut;
55 import org.apache.http.client.methods.HttpUriRequest;
56 import org.apache.http.config.Registry;
57 import org.apache.http.config.RegistryBuilder;
58 import org.apache.http.conn.socket.ConnectionSocketFactory;
59 import org.apache.http.conn.socket.PlainConnectionSocketFactory;
60 import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
61 import org.apache.http.impl.client.HttpClientBuilder;
62 import org.apache.http.impl.client.HttpClients;
63 import org.apache.http.impl.client.SystemDefaultCredentialsProvider;
64 import org.apache.http.impl.conn.BasicHttpClientConnectionManager;
65 import org.apache.http.ssl.SSLContexts;
66 import org.eclipse.jgit.annotations.NonNull;
67 import org.eclipse.jgit.transport.http.HttpConnection;
68 import org.eclipse.jgit.transport.http.apache.internal.HttpApacheText;
69 import org.eclipse.jgit.util.HttpSupport;
70 import org.eclipse.jgit.util.TemporaryBuffer;
71 import org.eclipse.jgit.util.TemporaryBuffer.LocalFile;
72 
73 /**
74  * A {@link org.eclipse.jgit.transport.http.HttpConnection} which uses
75  * {@link org.apache.http.client.HttpClient}
76  *
77  * @since 3.3
78  */
79 public class HttpClientConnection implements HttpConnection {
80 	HttpClient client;
81 
82 	URL url;
83 
84 	HttpUriRequest req;
85 
86 	HttpResponse resp = null;
87 
88 	String method = "GET"; //$NON-NLS-1$
89 
90 	private TemporaryBufferEntity entity;
91 
92 	private boolean isUsingProxy = false;
93 
94 	private Proxy proxy;
95 
96 	private Integer timeout = null;
97 
98 	private Integer readTimeout;
99 
100 	private Boolean followRedirects;
101 
102 	private HostnameVerifier hostnameverifier;
103 
104 	private SSLContext ctx;
105 
106 	private SSLConnectionSocketFactory socketFactory;
107 
108 	private boolean usePooling = true;
109 
getClient()110 	private HttpClient getClient() {
111 		if (client == null) {
112 			HttpClientBuilder clientBuilder = HttpClients.custom();
113 			RequestConfig.Builder configBuilder = RequestConfig.custom();
114 			if (proxy != null && !Proxy.NO_PROXY.equals(proxy)) {
115 				isUsingProxy = true;
116 				InetSocketAddress adr = (InetSocketAddress) proxy.address();
117 				clientBuilder.setProxy(
118 						new HttpHost(adr.getHostName(), adr.getPort()));
119 			}
120 			if (timeout != null) {
121 				configBuilder.setConnectTimeout(timeout.intValue());
122 			}
123 			if (readTimeout != null) {
124 				configBuilder.setSocketTimeout(readTimeout.intValue());
125 			}
126 			if (followRedirects != null) {
127 				configBuilder
128 						.setRedirectsEnabled(followRedirects.booleanValue());
129 			}
130 			boolean pooled = true;
131 			SSLConnectionSocketFactory sslConnectionFactory;
132 			if (socketFactory != null) {
133 				pooled = usePooling;
134 				sslConnectionFactory = socketFactory;
135 			} else {
136 				// Legacy implementation.
137 				pooled = (hostnameverifier == null);
138 				sslConnectionFactory = getSSLSocketFactory();
139 			}
140 			clientBuilder.setSSLSocketFactory(sslConnectionFactory);
141 			if (!pooled) {
142 				Registry<ConnectionSocketFactory> registry = RegistryBuilder
143 						.<ConnectionSocketFactory> create()
144 						.register("https", sslConnectionFactory)
145 						.register("http", PlainConnectionSocketFactory.INSTANCE)
146 						.build();
147 				clientBuilder.setConnectionManager(
148 						new BasicHttpClientConnectionManager(registry));
149 			}
150 			clientBuilder.setDefaultRequestConfig(configBuilder.build());
151 			clientBuilder.setDefaultCredentialsProvider(
152 					new SystemDefaultCredentialsProvider());
153 			client = clientBuilder.build();
154 		}
155 
156 		return client;
157 	}
158 
setSSLSocketFactory(@onNull SSLConnectionSocketFactory factory, boolean isDefault)159 	void setSSLSocketFactory(@NonNull SSLConnectionSocketFactory factory,
160 			boolean isDefault) {
161 		socketFactory = factory;
162 		usePooling = isDefault;
163 	}
164 
getSSLSocketFactory()165 	private SSLConnectionSocketFactory getSSLSocketFactory() {
166 		HostnameVerifier verifier = hostnameverifier;
167 		SSLContext context;
168 		if (verifier == null) {
169 			// Use defaults
170 			context = SSLContexts.createSystemDefault();
171 			verifier = SSLConnectionSocketFactory.getDefaultHostnameVerifier();
172 		} else {
173 			// Using a custom verifier. Attention: configure() must have been
174 			// called already, otherwise one gets a "context not initialized"
175 			// exception. In JGit this branch is reached only when hostname
176 			// verification is switched off, and JGit _does_ call configure()
177 			// before we get here.
178 			context = getSSLContext();
179 		}
180 		return new SSLConnectionSocketFactory(context, verifier) {
181 
182 			@Override
183 			protected void prepareSocket(SSLSocket socket) throws IOException {
184 				super.prepareSocket(socket);
185 				HttpSupport.configureTLS(socket);
186 			}
187 		};
188 	}
189 
190 	private SSLContext getSSLContext() {
191 		if (ctx == null) {
192 			try {
193 				ctx = SSLContext.getInstance("TLS"); //$NON-NLS-1$
194 			} catch (NoSuchAlgorithmException e) {
195 				throw new IllegalStateException(
196 						HttpApacheText.get().unexpectedSSLContextException, e);
197 			}
198 		}
199 		return ctx;
200 	}
201 
202 	/**
203 	 * Sets the buffer from which to take the request body
204 	 *
205 	 * @param buffer
206 	 */
207 	public void setBuffer(TemporaryBuffer buffer) {
208 		this.entity = new TemporaryBufferEntity(buffer);
209 	}
210 
211 	/**
212 	 * Constructor for HttpClientConnection.
213 	 *
214 	 * @param urlStr
215 	 * @throws MalformedURLException
216 	 */
217 	public HttpClientConnection(String urlStr) throws MalformedURLException {
218 		this(urlStr, null);
219 	}
220 
221 	/**
222 	 * Constructor for HttpClientConnection.
223 	 *
224 	 * @param urlStr
225 	 * @param proxy
226 	 * @throws MalformedURLException
227 	 */
228 	public HttpClientConnection(String urlStr, Proxy proxy)
229 			throws MalformedURLException {
230 		this(urlStr, proxy, null);
231 	}
232 
233 	/**
234 	 * Constructor for HttpClientConnection.
235 	 *
236 	 * @param urlStr
237 	 * @param proxy
238 	 * @param cl
239 	 * @throws MalformedURLException
240 	 */
241 	public HttpClientConnection(String urlStr, Proxy proxy, HttpClient cl)
242 			throws MalformedURLException {
243 		this.client = cl;
244 		this.url = new URL(urlStr);
245 		this.proxy = proxy;
246 	}
247 
248 	/** {@inheritDoc} */
249 	@Override
250 	public int getResponseCode() throws IOException {
251 		execute();
252 		return resp.getStatusLine().getStatusCode();
253 	}
254 
255 	/** {@inheritDoc} */
256 	@Override
257 	public URL getURL() {
258 		return url;
259 	}
260 
261 	/** {@inheritDoc} */
262 	@Override
263 	public String getResponseMessage() throws IOException {
264 		execute();
265 		return resp.getStatusLine().getReasonPhrase();
266 	}
267 
268 	private void execute() throws IOException, ClientProtocolException {
269 		if (resp != null) {
270 			return;
271 		}
272 
273 		if (entity == null) {
274 			resp = getClient().execute(req);
275 			return;
276 		}
277 
278 		try {
279 			if (req instanceof HttpEntityEnclosingRequest) {
280 				HttpEntityEnclosingRequest eReq = (HttpEntityEnclosingRequest) req;
281 				eReq.setEntity(entity);
282 			}
283 			resp = getClient().execute(req);
284 		} finally {
285 			entity.close();
286 			entity = null;
287 		}
288 	}
289 
290 	/** {@inheritDoc} */
291 	@Override
292 	public Map<String, List<String>> getHeaderFields() {
293 		Map<String, List<String>> ret = new HashMap<>();
294 		for (Header hdr : resp.getAllHeaders()) {
295 			List<String> list = ret.get(hdr.getName());
296 			if (list == null) {
297 				list = new LinkedList<>();
298 				ret.put(hdr.getName(), list);
299 			}
300 			for (HeaderElement hdrElem : hdr.getElements()) {
301 				list.add(hdrElem.toString());
302 			}
303 		}
304 		return ret;
305 	}
306 
307 	/** {@inheritDoc} */
308 	@Override
309 	public void setRequestProperty(String name, String value) {
310 		req.addHeader(name, value);
311 	}
312 
313 	/** {@inheritDoc} */
314 	@Override
315 	public void setRequestMethod(String method) throws ProtocolException {
316 		this.method = method;
317 		if (METHOD_GET.equalsIgnoreCase(method)) {
318 			req = new HttpGet(url.toString());
319 		} else if (METHOD_HEAD.equalsIgnoreCase(method)) {
320 			req = new HttpHead(url.toString());
321 		} else if (METHOD_PUT.equalsIgnoreCase(method)) {
322 			req = new HttpPut(url.toString());
323 		} else if (METHOD_POST.equalsIgnoreCase(method)) {
324 			req = new HttpPost(url.toString());
325 		} else {
326 			this.method = null;
327 			throw new UnsupportedOperationException();
328 		}
329 	}
330 
331 	/** {@inheritDoc} */
332 	@Override
333 	public void setUseCaches(boolean usecaches) {
334 		// not needed
335 	}
336 
337 	/** {@inheritDoc} */
338 	@Override
339 	public void setConnectTimeout(int timeout) {
340 		this.timeout = Integer.valueOf(timeout);
341 	}
342 
343 	/** {@inheritDoc} */
344 	@Override
345 	public void setReadTimeout(int readTimeout) {
346 		this.readTimeout = Integer.valueOf(readTimeout);
347 	}
348 
349 	/** {@inheritDoc} */
350 	@Override
351 	public String getContentType() {
352 		HttpEntity responseEntity = resp.getEntity();
353 		if (responseEntity != null) {
354 			Header contentType = responseEntity.getContentType();
355 			if (contentType != null)
356 				return contentType.getValue();
357 		}
358 		return null;
359 	}
360 
361 	/** {@inheritDoc} */
362 	@Override
363 	public InputStream getInputStream() throws IOException {
364 		execute();
365 		return resp.getEntity().getContent();
366 	}
367 
368 	// will return only the first field
369 	/** {@inheritDoc} */
370 	@Override
371 	public String getHeaderField(@NonNull String name) {
372 		Header header = resp.getFirstHeader(name);
373 		return (header == null) ? null : header.getValue();
374 	}
375 
376 	@Override
377 	public List<String> getHeaderFields(@NonNull String name) {
378 		return Collections.unmodifiableList(Arrays.asList(resp.getHeaders(name))
379 				.stream().map(Header::getValue).collect(Collectors.toList()));
380 	}
381 
382 	/** {@inheritDoc} */
383 	@Override
384 	public int getContentLength() {
385 		Header contentLength = resp.getFirstHeader("content-length"); //$NON-NLS-1$
386 		if (contentLength == null) {
387 			return -1;
388 		}
389 
390 		try {
391 			int l = Integer.parseInt(contentLength.getValue());
392 			return l < 0 ? -1 : l;
393 		} catch (NumberFormatException e) {
394 			return -1;
395 		}
396 	}
397 
398 	/** {@inheritDoc} */
399 	@Override
400 	public void setInstanceFollowRedirects(boolean followRedirects) {
401 		this.followRedirects = Boolean.valueOf(followRedirects);
402 	}
403 
404 	/** {@inheritDoc} */
405 	@Override
406 	public void setDoOutput(boolean dooutput) {
407 		// TODO: check whether we can really ignore this.
408 	}
409 
410 	/** {@inheritDoc} */
411 	@Override
412 	public void setFixedLengthStreamingMode(int contentLength) {
413 		if (entity != null)
414 			throw new IllegalArgumentException();
415 		entity = new TemporaryBufferEntity(new LocalFile(null));
416 		entity.setContentLength(contentLength);
417 	}
418 
419 	/** {@inheritDoc} */
420 	@Override
421 	public OutputStream getOutputStream() throws IOException {
422 		if (entity == null)
423 			entity = new TemporaryBufferEntity(new LocalFile(null));
424 		return entity.getBuffer();
425 	}
426 
427 	/** {@inheritDoc} */
428 	@Override
429 	public void setChunkedStreamingMode(int chunklen) {
430 		if (entity == null)
431 			entity = new TemporaryBufferEntity(new LocalFile(null));
432 		entity.setChunked(true);
433 	}
434 
435 	/** {@inheritDoc} */
436 	@Override
437 	public String getRequestMethod() {
438 		return method;
439 	}
440 
441 	/** {@inheritDoc} */
442 	@Override
443 	public boolean usingProxy() {
444 		return isUsingProxy;
445 	}
446 
447 	/** {@inheritDoc} */
448 	@Override
449 	public void connect() throws IOException {
450 		execute();
451 	}
452 
453 	/** {@inheritDoc} */
454 	@Override
455 	public void setHostnameVerifier(HostnameVerifier hostnameverifier) {
456 		this.hostnameverifier = hostnameverifier;
457 	}
458 
459 	/** {@inheritDoc} */
460 	@Override
461 	public void configure(KeyManager[] km, TrustManager[] tm,
462 			SecureRandom random) throws KeyManagementException {
463 		getSSLContext().init(km, tm, random);
464 	}
465 }
466