1 package com.atlassian.marketplace.client.impl;
2
3 import java.io.Closeable;
4 import java.io.IOException;
5 import java.io.InputStream;
6 import java.io.UnsupportedEncodingException;
7 import java.net.SocketException;
8 import java.net.URI;
9 import java.util.ArrayList;
10 import java.util.List;
11 import java.util.Map;
12
13 import com.atlassian.fugue.Option;
14 import com.atlassian.marketplace.client.MarketplaceClient;
15 import com.atlassian.marketplace.client.MpacException;
16 import com.atlassian.marketplace.client.http.HttpConfiguration;
17 import com.atlassian.marketplace.client.http.HttpConfiguration.ProxyAuthMethod;
18 import com.atlassian.marketplace.client.http.HttpConfiguration.ProxyConfiguration;
19 import com.atlassian.marketplace.client.http.HttpTransport;
20 import com.atlassian.marketplace.client.http.RequestDecorator;
21 import com.atlassian.marketplace.client.http.SimpleHttpResponse;
22
23 import com.google.common.collect.ImmutableList;
24 import com.google.common.collect.Multimap;
25
26 import org.apache.http.Consts;
27 import org.apache.http.Header;
28 import org.apache.http.HttpHost;
29 import org.apache.http.HttpRequest;
30 import org.apache.http.HttpRequestInterceptor;
31 import org.apache.http.HttpResponse;
32 import org.apache.http.NameValuePair;
33 import org.apache.http.auth.AUTH;
34 import org.apache.http.auth.AuthScope;
35 import org.apache.http.auth.Credentials;
36 import org.apache.http.auth.NTCredentials;
37 import org.apache.http.auth.UsernamePasswordCredentials;
38 import org.apache.http.client.AuthCache;
39 import org.apache.http.client.CredentialsProvider;
40 import org.apache.http.client.HttpClient;
41 import org.apache.http.client.config.RequestConfig;
42 import org.apache.http.client.entity.UrlEncodedFormEntity;
43 import org.apache.http.client.methods.HttpDelete;
44 import org.apache.http.client.methods.HttpGet;
45 import org.apache.http.client.methods.HttpPatch;
46 import org.apache.http.client.methods.HttpPost;
47 import org.apache.http.client.methods.HttpPut;
48 import org.apache.http.client.methods.HttpUriRequest;
49 import org.apache.http.entity.ByteArrayEntity;
50 import org.apache.http.entity.ContentType;
51 import org.apache.http.entity.InputStreamEntity;
52 import org.apache.http.impl.auth.BasicScheme;
53 import org.apache.http.impl.client.BasicAuthCache;
54 import org.apache.http.impl.client.CloseableHttpClient;
55 import org.apache.http.impl.client.HttpClientBuilder;
56 import org.apache.http.impl.client.cache.CacheConfig;
57 import org.apache.http.impl.client.cache.CachingHttpClientBuilder;
58 import org.apache.http.message.BasicHeader;
59 import org.apache.http.message.BasicNameValuePair;
60 import org.apache.http.protocol.HttpContext;
61 import org.slf4j.Logger;
62 import org.slf4j.LoggerFactory;
63
64 import static com.atlassian.fugue.Option.none;
65 import static com.atlassian.fugue.Option.option;
66 import static com.atlassian.fugue.Option.some;
67 import static com.google.common.base.Preconditions.checkNotNull;
68 import static com.google.common.collect.Iterables.concat;
69 import static java.lang.System.getProperty;
70 import static org.apache.commons.lang.StringUtils.trimToNull;
71 import static org.apache.http.client.config.CookieSpecs.IGNORE_COOKIES;
72 import static org.apache.http.client.protocol.HttpClientContext.AUTH_CACHE;
73 import static org.apache.http.entity.ContentType.APPLICATION_JSON;
74 import static org.apache.http.protocol.HTTP.CONTENT_TYPE;
75 import static org.apache.http.protocol.HttpCoreContext.HTTP_TARGET_HOST;
76 import static org.apache.http.util.EntityUtils.consumeQuietly;
77
78
79
80
81
82 public final class CommonsHttpTransport implements HttpTransport
83 {
84 private static final Logger logger = LoggerFactory.getLogger(MarketplaceClient.class);
85 private static final ContentType APPLICATION_JSON_PATCH =
86 ContentType.create("application/json-patch+json", Consts.UTF_8);
87
88 private final HttpClient client;
89 private final HttpConfiguration config;
90 private final HttpTransport defaultOperations;
91
92
93
94
95
96 public enum CachingBehavior
97 {
98 NO_CACHING,
99 CACHING
100 }
101
102 public CommonsHttpTransport(HttpConfiguration configuration, URI baseUri)
103 {
104 this.config = checkNotNull(configuration, "configuration");
105 this.client = httpClientBuilder(config, some(baseUri), CachingBehavior.CACHING).build();
106 this.defaultOperations = new OperationsImpl(ImmutableList.<RequestDecorator>of());
107 }
108
109 @Override
110 public SimpleHttpResponse get(URI uri) throws MpacException
111 {
112 return defaultOperations.get(uri);
113 }
114
115 @Override
116 public SimpleHttpResponse postParams(URI uri, Multimap<String, String> params) throws MpacException
117 {
118 return defaultOperations.postParams(uri, params);
119 }
120
121 @Override
122 public SimpleHttpResponse post(URI uri, InputStream content, long length, String contentType, String acceptContentType) throws MpacException
123 {
124 return defaultOperations.post(uri, content, length, contentType, acceptContentType);
125 }
126
127 @Override
128 public SimpleHttpResponse put(URI uri, byte[] content) throws MpacException
129 {
130 return defaultOperations.put(uri, content);
131 }
132
133 @Override
134 public SimpleHttpResponse patch(URI uri, byte[] content) throws MpacException
135 {
136 return defaultOperations.patch(uri, content);
137 }
138
139 @Override
140 public SimpleHttpResponse delete(URI uri) throws MpacException
141 {
142 return defaultOperations.delete(uri);
143 }
144
145 @Override
146 public HttpTransport withRequestDecorator(RequestDecorator decorator)
147 {
148 return defaultOperations.withRequestDecorator(decorator);
149 }
150
151
152
153
154
155 @Override
156 public void close()
157 {
158 if (client instanceof Closeable)
159 {
160 try
161 {
162 ((CloseableHttpClient) client).close();
163 }
164 catch (IOException e)
165 {
166 logger.warn("Unexpected error while closing HTTP client: " + e);
167 logger.debug(e.toString(), e);
168 }
169 }
170 }
171
172
173
174
175
176
177
178
179
180
181
182 public static HttpClient createHttpClient(HttpConfiguration config, Option<URI> baseUri)
183 {
184 return httpClientBuilder(config, baseUri, CachingBehavior.NO_CACHING).build();
185 }
186
187
188
189
190
191
192
193
194
195
196
197
198 public static HttpClientBuilder httpClientBuilder(HttpConfiguration config,
199 Option<URI> baseUri, CachingBehavior cachingBehavior)
200 {
201 HttpClientBuilder builder;
202 if (cachingBehavior == CachingBehavior.CACHING)
203 {
204 CachingHttpClientBuilder cachingBuilder = CachingHttpClientBuilder.create();
205 CacheConfig.Builder configBuilder = CacheConfig.custom();
206 configBuilder.setSharedCache(false);
207 configBuilder.setMaxCacheEntries(config.getMaxCacheEntries());
208 configBuilder.setMaxObjectSize(config.getMaxCacheObjectSize());
209 cachingBuilder.setCacheConfig(configBuilder.build());
210 builder = cachingBuilder;
211 }
212 else
213 {
214 builder = HttpClientBuilder.create();
215 }
216 builder.useSystemProperties();
217 builder.setMaxConnPerRoute(config.getMaxConnections());
218
219 RequestConfig.Builder rc = RequestConfig.custom()
220 .setConnectTimeout(config.getConnectTimeoutMillis())
221 .setSocketTimeout(config.getReadTimeoutMillis())
222 .setCookieSpec(IGNORE_COOKIES)
223 .setProxyPreferredAuthSchemes(getProxyPreferredAuthSchemes(config));
224 for (int maxRedirects: config.getMaxRedirects())
225 {
226 rc.setMaxRedirects(maxRedirects);
227 }
228 builder.setDefaultRequestConfig(rc.build());
229
230 Option<HttpConfiguration.ProxyHost> realProxyHost = getRealProxyHost(config, baseUri);
231 for (HttpConfiguration.ProxyHost ph: realProxyHost)
232 {
233 builder.setProxy(new HttpHost(ph.getHostname(), ph.getPort()));
234 }
235
236 builder.addInterceptorFirst(new DefaultRequestInterceptor(config, realProxyHost));
237 builder.setDefaultCredentialsProvider(new DefaultCredentialsProvider(config, realProxyHost));
238
239 return builder;
240 }
241
242 private static Option<HttpConfiguration.ProxyHost> getRealProxyHost(HttpConfiguration config, Option<URI> baseUri)
243 {
244 for (ProxyConfiguration proxy: config.getProxyConfiguration())
245 {
246 if (proxy.getProxyHost().isDefined())
247 {
248 return proxy.getProxyHost();
249 }
250 String prefix = "https";
251 for (URI u: baseUri)
252 {
253 if (u.getScheme() != null && u.getScheme().equalsIgnoreCase("http"))
254 {
255 prefix = "http";
256 }
257 }
258 for (String host: option(trimToNull(getProperty(prefix + ".proxyHost"))))
259 {
260 int port = Integer.parseInt(getProperty(prefix + ".proxyPort", String.valueOf(HttpConfiguration.ProxyHost.DEFAULT_PORT)));
261 return some(new HttpConfiguration.ProxyHost(host, port));
262 }
263 }
264 return none();
265 }
266
267 private static ImmutableList<String> getProxyPreferredAuthSchemes(HttpConfiguration config)
268 {
269 for (ProxyConfiguration proxy: config.getProxyConfiguration())
270 {
271 for (HttpConfiguration.ProxyAuthParams auth: proxy.getAuthParams())
272 {
273 return ImmutableList.of(auth.getAuthMethod().name().toUpperCase());
274 }
275 }
276 return ImmutableList.of();
277 }
278
279 private class OperationsImpl implements HttpTransport
280 {
281 private final Iterable<RequestDecorator> decorators;
282
283 OperationsImpl(Iterable<RequestDecorator> decorators)
284 {
285 this.decorators = ImmutableList.copyOf(decorators);
286 }
287
288 @Override
289 public HttpTransport withRequestDecorator(RequestDecorator decorator)
290 {
291 return new OperationsImpl(concat(decorators, ImmutableList.of(decorator)));
292 }
293
294 @Override
295 public SimpleHttpResponse get(URI uri) throws MpacException
296 {
297 HttpGet method = new HttpGet(uri);
298 return executeMethod(method);
299 }
300
301 @Override
302 public SimpleHttpResponse postParams(URI uri, Multimap<String, String> params) throws MpacException
303 {
304 HttpPost method = new HttpPost(uri);
305 List<NameValuePair> formParams = new ArrayList<NameValuePair>();
306 for (Map.Entry<String, String> param: params.entries())
307 {
308 formParams.add(new BasicNameValuePair(param.getKey(), param.getValue()));
309 }
310 try
311 {
312 method.setEntity(new UrlEncodedFormEntity(formParams));
313 }
314 catch (UnsupportedEncodingException e)
315 {
316 throw new MpacException(e);
317 }
318 return executeMethod(method);
319 }
320
321 @Override
322 public SimpleHttpResponse post(URI uri, InputStream content, long length, String contentType, String acceptContentType) throws MpacException
323 {
324 HttpPost method = new HttpPost(uri);
325 method.setEntity(new InputStreamEntity(content, length, ContentType.create(contentType)));
326 method.addHeader(CONTENT_TYPE, contentType);
327 method.addHeader("Accept", acceptContentType);
328 return executeMethod(method);
329 }
330
331 @Override
332 public SimpleHttpResponse put(URI uri, byte[] content) throws MpacException
333 {
334 HttpPut method = new HttpPut(uri);
335 method.setEntity(new ByteArrayEntity(content, APPLICATION_JSON));
336 return executeMethod(method);
337 }
338
339 @Override
340 public SimpleHttpResponse patch(URI uri, byte[] content) throws MpacException
341 {
342 HttpPatch method = new HttpPatch(uri);
343 method.setEntity(new ByteArrayEntity(content, APPLICATION_JSON_PATCH));
344 return executeMethod(method);
345 }
346
347 @Override
348 public SimpleHttpResponse delete(URI uri) throws MpacException
349 {
350 HttpDelete method = new HttpDelete(uri);
351 return executeMethod(method);
352 }
353
354 @Override
355 public void close()
356 {
357 CommonsHttpTransport.this.close();
358 }
359
360 private SimpleHttpResponse executeMethod(HttpUriRequest method) throws MpacException
361 {
362 logger.info(method.getMethod() + " " + method.getURI());
363 for (RequestDecorator rd: decorators)
364 {
365 Map<String, String> moreHeaders = rd.getRequestHeaders();
366 if (moreHeaders != null)
367 {
368 for (Map.Entry<String, String> header: moreHeaders.entrySet())
369 {
370 method.addHeader(header.getKey(), header.getValue());
371 }
372 }
373 }
374 try
375 {
376 return new ResponseImpl(client.execute(method));
377 }
378 catch (SocketException e)
379 {
380 throw new MpacException.ConnectionFailure(e);
381 }
382 catch (IOException e)
383 {
384 throw new MpacException(e);
385 }
386 }
387 }
388
389 private static class ResponseImpl implements SimpleHttpResponse
390 {
391 private final HttpResponse response;
392
393 ResponseImpl(HttpResponse response)
394 {
395 this.response = response;
396 }
397
398 public int getStatus()
399 {
400 return response.getStatusLine().getStatusCode();
401 }
402
403 public Iterable<String> getHeader(String name)
404 {
405 ImmutableList.Builder<String> ret = ImmutableList.builder();
406 for (Header h: response.getHeaders(name))
407 {
408 ret.add(h.getValue());
409 }
410 return ret.build();
411 }
412
413 public InputStream getContentStream() throws MpacException
414 {
415 try
416 {
417 return response.getEntity().getContent();
418 }
419 catch (IOException e)
420 {
421 throw new MpacException(e);
422 }
423 }
424
425 public boolean isEmpty()
426 {
427 Header h = response.getFirstHeader("Content-Length");
428 return (h != null) && (h.getValue().trim().equals("0"));
429 }
430
431 public void close()
432 {
433 if (response.getEntity() != null)
434 {
435 consumeQuietly(response.getEntity());
436 }
437 }
438 }
439
440 private static class DefaultRequestInterceptor implements HttpRequestInterceptor
441 {
442 private final HttpConfiguration config;
443 private final Option<HttpConfiguration.ProxyHost> proxyHost;
444
445 DefaultRequestInterceptor(HttpConfiguration config, Option<HttpConfiguration.ProxyHost> proxyHost)
446 {
447 this.config = checkNotNull(config);
448 this.proxyHost = checkNotNull(proxyHost);
449 }
450
451 @Override
452 public void process(HttpRequest request, HttpContext context)
453 {
454 AuthCache authCache = null;
455
456 for (RequestDecorator rd: config.getRequestDecorator())
457 {
458 Map<String, String> headers = rd.getRequestHeaders();
459 if (headers != null)
460 {
461 for (Map.Entry<String, String> header: headers.entrySet())
462 {
463 request.addHeader(header.getKey(), header.getValue());
464 }
465 }
466 }
467
468 if (config.hasCredentials())
469 {
470
471 if (authCache == null)
472 {
473 authCache = new BasicAuthCache();
474 }
475 HttpHost targetHost = (HttpHost) context.getAttribute(HTTP_TARGET_HOST);
476 if (targetHost != null)
477 {
478 authCache.put(targetHost, new BasicScheme());
479 }
480 }
481
482 for (ProxyConfiguration proxy: config.getProxyConfiguration())
483 {
484 for (HttpConfiguration.ProxyAuthParams auth: proxy.getAuthParams())
485 {
486
487 if (auth.getAuthMethod() == ProxyAuthMethod.BASIC)
488 {
489 for (HttpConfiguration.ProxyHost ph: proxyHost)
490 {
491
492
493
494 HttpHost hph = new HttpHost(ph.getHostname(), ph.getPort());
495 if (authCache == null)
496 {
497 authCache = new BasicAuthCache();
498 }
499 BasicScheme proxyAuth = new BasicScheme();
500 try
501 {
502 proxyAuth.processChallenge(new BasicHeader(AUTH.PROXY_AUTH, "BASIC realm=default"));
503 authCache.put(hph, proxyAuth);
504 }
505 catch (Exception e)
506 {
507 logger.warn("Error, unable to set preemptive proxy auth: " + e);
508 logger.debug(e.toString(), e);
509 }
510 }
511 }
512 }
513 }
514
515 if (authCache != null)
516 {
517 context.setAttribute(AUTH_CACHE, authCache);
518 }
519 }
520 }
521
522 private static class DefaultCredentialsProvider implements CredentialsProvider
523 {
524 private final Option<HttpConfiguration.ProxyHost> proxyHost;
525 private final Option<Credentials> proxyCredentials;
526 private final Option<Credentials> targetHostCredentials;
527
528 DefaultCredentialsProvider(HttpConfiguration config, Option<HttpConfiguration.ProxyHost> proxyHost)
529 {
530 this.proxyCredentials = makeProxyCredentials(config);
531 this.targetHostCredentials = makeTargetHostCredentials(config);
532 this.proxyHost = checkNotNull(proxyHost);
533 }
534
535 @Override
536 public void setCredentials(AuthScope authscope, Credentials credentials)
537 {
538
539 }
540
541 @Override
542 public Credentials getCredentials(AuthScope authScope)
543 {
544 for (HttpConfiguration.ProxyHost ph: proxyHost)
545 {
546 if (ph.getHostname().equals(authScope.getHost()) && ph.getPort() == authScope.getPort())
547 {
548 return proxyCredentials.getOrElse((Credentials) null);
549 }
550 }
551
552 return targetHostCredentials.getOrElse((Credentials) null);
553 }
554
555 @Override
556 public void clear()
557 {
558 }
559
560 private Option<Credentials> makeProxyCredentials(HttpConfiguration config)
561 {
562 for (HttpConfiguration.ProxyConfiguration proxy: config.getProxyConfiguration())
563 {
564 for (HttpConfiguration.ProxyAuthParams auth: proxy.getAuthParams())
565 {
566 Credentials c;
567 switch (auth.getAuthMethod())
568 {
569 case NTLM:
570 c = new NTCredentials(auth.getCredentials().getUsername(),
571 auth.getCredentials().getPassword(),
572 auth.getNtlmWorkstation().getOrElse(""),
573 auth.getNtlmDomain().getOrElse(""));
574
575 default:
576 c = new UsernamePasswordCredentials(auth.getCredentials().getUsername(),
577 auth.getCredentials().getPassword());
578 }
579 return some(c);
580 }
581 }
582 return none();
583 }
584
585 private Option<Credentials> makeTargetHostCredentials(HttpConfiguration config)
586 {
587 for (HttpConfiguration.Credentials c: config.getCredentials())
588 {
589 return Option.<Credentials>some(new UsernamePasswordCredentials(c.getUsername(), c.getPassword()));
590 }
591 return none();
592 }
593 }
594 }