View Javadoc

1   package com.atlassian.httpclient.apache.httpcomponents.proxy;
2   
3   import com.atlassian.fugue.Option;
4   import com.atlassian.fugue.Options;
5   import com.atlassian.httpclient.api.factory.Host;
6   import com.atlassian.httpclient.api.factory.Scheme;
7   import com.google.common.base.Preconditions;
8   import com.google.common.collect.Iterables;
9   import com.google.common.collect.Lists;
10  import org.apache.http.HttpHost;
11  import org.slf4j.Logger;
12  import org.slf4j.LoggerFactory;
13  
14  import javax.annotation.Nonnull;
15  import java.io.IOException;
16  import java.net.*;
17  import java.util.*;
18  import java.util.function.Predicate;
19  import java.util.regex.Pattern;
20  import java.util.regex.PatternSyntaxException;
21  import java.util.stream.Collectors;
22  
23  /**
24   * HttpClientProxyConfig implementation that uses proxy configuration from construction, and not
25   * from system properties.
26   *
27   * @since 0.20.0
28   */
29  public class ProvidedProxyConfig extends ProxyConfig {
30      private static final Logger log = LoggerFactory.getLogger(ProvidedProxyConfig.class);
31  
32      private static final List<Proxy> NO_PROXIES = Collections.singletonList(Proxy.NO_PROXY);
33      private static final Iterable<String> SUPPORTED_SCHEMAS = Lists.newArrayList("http", "https");
34  
35      private final Map<String, HttpHost> proxyHostMap;
36      private final Map<String, Predicate<String>> nonProxyHosts;
37  
38      public ProvidedProxyConfig(@Nonnull final Map<Scheme, Host> proxyHostMap,
39                                 @Nonnull final Map<Scheme, List<String>> nonProxyHosts) {
40          Preconditions.checkNotNull(proxyHostMap);
41          Preconditions.checkNotNull(nonProxyHosts);
42          this.proxyHostMap = new HashMap<>(proxyHostMap.size());
43          for (Scheme s : proxyHostMap.keySet()) {
44              Host h = proxyHostMap.get(s);
45              this.proxyHostMap.put(s.schemeName(), new HttpHost(h.getHost(), h.getPort()));
46          }
47          this.nonProxyHosts = new HashMap<>(nonProxyHosts.size());
48          for (Scheme scheme : nonProxyHosts.keySet()) {
49              List<String> nonProxyHostList = nonProxyHosts.get(scheme);
50              if (nonProxyHostList != null) {
51                  Pattern wildcardHostsPattern = getWildcardHostsPattern(nonProxyHostList);
52                  Set<String> literalHosts = getLiteralHosts(nonProxyHostList);
53  
54                  this.nonProxyHosts.put(scheme.schemeName(), host ->
55                          literalHosts.contains(host) ||
56                          wildcardHostsPattern != null && wildcardHostsPattern.matcher(host).matches());
57              }
58          }
59      }
60  
61      private Set<String> getLiteralHosts(List<String> nonProxyHosts) {
62          Set<String> literalHosts = nonProxyHosts.stream()
63                  .filter(host -> host.indexOf('*') == -1)
64                  .map(String::toLowerCase)
65                  .collect(Collectors.toSet());
66          log.trace("Literal hosts for http.nonProxyHost: {}", literalHosts);
67          return literalHosts;
68      }
69  
70      private Pattern getWildcardHostsPattern(List<String> nonProxyHosts) {
71          String compoundPattern = nonProxyHosts.stream()
72                  .filter(host -> host.indexOf('*') != -1)
73                  .map(String::toLowerCase)
74                  .map(this::hostWildcardToPattern)
75                  .filter(Objects::nonNull)
76                  .map(subPattern -> "(:?" + subPattern + ")")
77                  .collect(Collectors.joining("|"));
78          try {
79              if (compoundPattern.isEmpty()) {
80                  return null;
81              }
82  
83              log.trace("Compound pattern for http.nonProxyHost wildcard values {}: {}",
84                      nonProxyHosts, compoundPattern);
85              return Pattern.compile(compoundPattern);
86          } catch (PatternSyntaxException e) {
87              log.warn("Ignoring http.nonProxyHost values \"{}\" because converting these to a regular expression failed",
88                      nonProxyHosts, e);
89              return null;
90          }
91      }
92  
93      @Override
94      Iterable<HttpHost> getProxyHosts() {
95          final Iterable<Option<HttpHost>> httpHosts = Iterables.transform(SUPPORTED_SCHEMAS,
96                  schema -> Option.option(proxyHostMap.get(schema)));
97          return Options.flatten(Options.filterNone(httpHosts));
98      }
99  
100     @Override
101     public Iterable<AuthenticationInfo> getAuthenticationInfo() {
102         log.info("Authentication info not supported for ProvidedProxyConfig");
103         return Collections.emptyList();
104     }
105 
106     @Override
107     public ProxySelector toProxySelector() {
108         return new ProxySelector() {
109             @Override
110             public List<Proxy> select(URI uri) {
111                 String scheme = uri.getScheme().toLowerCase();
112                 String host = uri.getHost().toLowerCase();
113 
114                 HttpHost proxyHost = proxyHostMap.get(scheme);
115                 if (proxyHost == null) {
116                     return NO_PROXIES;
117                 }
118 
119                 if (nonProxyMatch(scheme, host)) {
120                     return NO_PROXIES;
121                 }
122 
123                 // HTTP *OR* HTTPS despite what Proxy.Type.HTTP implies
124                 return Collections.singletonList(new Proxy(Proxy.Type.HTTP,
125                         InetSocketAddress.createUnresolved(proxyHost.getHostName(), proxyHost.getPort())));
126             }
127 
128             @Override
129             public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
130                 // ignore
131             }
132         };
133     }
134 
135     @SuppressWarnings("ResultOfMethodCallIgnored")
136     private String compileHostPattern(String wildcardHost, String pattern) {
137         String regex = "^" + pattern + "$";
138         try {
139             // compile the pattern but don't return it - we build a multi-pattern but don't want to abort the whole
140             // thing if only one part fails for some reason
141             Pattern.compile(regex);
142         } catch (PatternSyntaxException e) {
143             log.warn("Ignoring http.nonProxyHost \"{}\" because converting it to a regular expression failed", wildcardHost, e);
144             return null;
145         }
146         return regex;
147     }
148 
149     private String hostWildcardToPattern(String wildcardHost) {
150         // to stay consistent with what ProxySelector.getDefault() returns for system properties, only the
151         // leading/trailing * is noticed and only one or the other, not both
152         if (wildcardHost.startsWith("*")) {
153             return compileHostPattern(wildcardHost, ".*" + Pattern.quote(wildcardHost.substring(1)));
154         } else if (wildcardHost.endsWith("*")) {
155             return compileHostPattern(wildcardHost, Pattern.quote(wildcardHost.substring(0, wildcardHost.length() - 1)) + ".*");
156         } else {
157             return compileHostPattern(wildcardHost, Pattern.quote(wildcardHost));
158         }
159     }
160 
161     private Boolean nonProxyMatch(String scheme, String host) {
162         return Optional.ofNullable(nonProxyHosts.get(scheme)).map(predicate -> predicate.test(host)).orElse(false);
163     }
164 }