View Javadoc

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   * Implementation of {@link HttpTransport} based on Apache HttpComponents.
80   * @since 2.0.0
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       * Use with {@link CommonsHttpTransport#createHttpClient} to indicate whether or not to
94       * enable HTTP caching.
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      * Should be called to shutdown the Client HttpConnectionManager, this will prevent finaliser errors
153      * from appearing in the log, and helps to prevent a resource leak.
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      * Helper method that configures an HttpClient with the specified proxy/timeout properties.
174      * All requests done through the resulting client will use the configured settings, in terms of
175      * timeouts, proxies, {@link RequestDecorator}s, etc.
176      *  
177      * @param config  an {@link HttpConfiguration}
178      * @param baseUri  base URI of the Marketplace server; this is only relevant if you're providing
179      * basicauth credentials for the server itself (rather than for the proxy)
180      * @return  a configured HttpClient
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      * Helper method that configures an HttpClientBuilder with the specified proxy/timeout properties.
189      * All requests done through the resulting client will use the configured settings, in terms of
190      * timeouts, proxies, {@link RequestDecorator}s, etc.
191      *  
192      * @param config  an {@link HttpConfiguration}
193      * @param baseUri  base URI of the Marketplace server; this is only relevant if you're providing
194      * basicauth credentials for the server itself (rather than for the proxy)
195      * @param cachingBehavior  determines whether to use a caching client
196      * @return  a configured HttpClientBuilder
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                 // enable preemptive authentication for this request
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                     // If using basicauth for the proxy, send credentials preemptively to avoid a challenge
487                     if (auth.getAuthMethod() == ProxyAuthMethod.BASIC)
488                     {
489                         for (HttpConfiguration.ProxyHost ph: proxyHost)
490                         {
491                             // The following method of causing preemptive proxy authentication is better than simply
492                             // setting the Proxy-Authorization header on the request, because it will (or should)
493                             // work even for a tunneled request.  See: http://stackoverflow.com/questions/21121121/preemptive-proxy-authentication-with-http-tunnel-https-connection-in-apache-ht
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             // unused, this object is immutable
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 }