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