View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.eclipse.aether.transport.apache;
20  
21  import javax.net.ssl.HostnameVerifier;
22  import javax.net.ssl.SSLSocketFactory;
23  
24  import java.io.Closeable;
25  import java.util.Arrays;
26  import java.util.Iterator;
27  import java.util.Map;
28  import java.util.concurrent.ConcurrentHashMap;
29  import java.util.concurrent.ConcurrentMap;
30  import java.util.concurrent.TimeUnit;
31  
32  import org.apache.http.config.RegistryBuilder;
33  import org.apache.http.conn.HttpClientConnectionManager;
34  import org.apache.http.conn.socket.ConnectionSocketFactory;
35  import org.apache.http.conn.socket.PlainConnectionSocketFactory;
36  import org.apache.http.conn.ssl.NoopHostnameVerifier;
37  import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
38  import org.apache.http.impl.conn.DefaultHttpClientConnectionOperator;
39  import org.apache.http.impl.conn.DefaultSchemePortResolver;
40  import org.apache.http.impl.conn.ManagedHttpClientConnectionFactory;
41  import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
42  import org.apache.http.impl.conn.SystemDefaultDnsResolver;
43  import org.apache.http.ssl.SSLContextBuilder;
44  import org.apache.http.ssl.SSLInitializationException;
45  import org.eclipse.aether.ConfigurationProperties;
46  import org.eclipse.aether.Keys;
47  import org.eclipse.aether.RepositoryCache;
48  import org.eclipse.aether.RepositorySystemSession;
49  import org.eclipse.aether.util.ConfigUtils;
50  
51  /**
52   * Container for HTTP-related state that can be shared across incarnations of the transporter to optimize the
53   * communication with servers.
54   */
55  final class GlobalState implements Closeable {
56  
57      static {
58          // force initialization of SSLConnectionSocketFactory class:
59          // ensure that the connection socket factory is initialized before we start using it in any multithreaded
60          // environment, otherwise we may run into a deadlock.
61          // References:
62          // https://github.com/quarkusio/quarkus/issues/55317
63          // https://github.com/quarkusio/quarkus/pull/55345
64          SSLConnectionSocketFactory.getDefaultHostnameVerifier();
65      }
66  
67      static class CompoundKey {
68  
69          private final Object[] keys;
70  
71          CompoundKey(Object... keys) {
72              this.keys = keys;
73          }
74  
75          @Override
76          public boolean equals(Object obj) {
77              if (this == obj) {
78                  return true;
79              }
80              if (obj == null || !getClass().equals(obj.getClass())) {
81                  return false;
82              }
83              CompoundKey that = (CompoundKey) obj;
84              return Arrays.equals(keys, that.keys);
85          }
86  
87          @Override
88          public int hashCode() {
89              int hash = 17;
90              hash = hash * 31 + Arrays.hashCode(keys);
91              return hash;
92          }
93  
94          @Override
95          public String toString() {
96              return Arrays.toString(keys);
97          }
98      }
99  
100     private static final Object KEY = Keys.of(GlobalState.class);
101 
102     private static final String CONFIG_PROP_CACHE_STATE =
103             ApacheTransporterConfigurationKeys.CONFIG_PROPS_PREFIX + "cacheState";
104 
105     private final ConcurrentMap<ConnMgrConfig, HttpClientConnectionManager> connectionManagers;
106 
107     private final ConcurrentMap<CompoundKey, Object> userTokens;
108 
109     private final ConcurrentMap<CompoundKey, Boolean> expectContinues;
110 
111     public static GlobalState get(RepositorySystemSession session) {
112         GlobalState cache;
113         RepositoryCache repoCache = session.getCache();
114         if (repoCache == null || !ConfigUtils.getBoolean(session, true, CONFIG_PROP_CACHE_STATE)) {
115             cache = null;
116         } else {
117             Object tmp = repoCache.get(session, KEY);
118             if (tmp instanceof GlobalState) {
119                 cache = (GlobalState) tmp;
120             } else {
121                 synchronized (GlobalState.class) {
122                     tmp = repoCache.get(session, KEY);
123                     if (tmp instanceof GlobalState) {
124                         cache = (GlobalState) tmp;
125                     } else {
126                         cache = new GlobalState();
127                         repoCache.put(session, KEY, cache);
128                     }
129                 }
130             }
131         }
132         return cache;
133     }
134 
135     private GlobalState() {
136         connectionManagers = new ConcurrentHashMap<>();
137         userTokens = new ConcurrentHashMap<>();
138         expectContinues = new ConcurrentHashMap<>();
139     }
140 
141     @Override
142     public void close() {
143         for (Iterator<Map.Entry<ConnMgrConfig, HttpClientConnectionManager>> it =
144                         connectionManagers.entrySet().iterator();
145                 it.hasNext(); ) {
146             HttpClientConnectionManager connMgr = it.next().getValue();
147             it.remove();
148             connMgr.shutdown();
149         }
150     }
151 
152     public HttpClientConnectionManager getConnectionManager(ConnMgrConfig config) {
153         return connectionManagers.computeIfAbsent(config, GlobalState::newConnectionManager);
154     }
155 
156     public static HttpClientConnectionManager newConnectionManager(ConnMgrConfig connMgrConfig) {
157         RegistryBuilder<ConnectionSocketFactory> registryBuilder = RegistryBuilder.<ConnectionSocketFactory>create()
158                 .register("http", PlainConnectionSocketFactory.getSocketFactory());
159         int connectionMaxTtlSeconds = ConfigurationProperties.DEFAULT_HTTP_CONNECTION_MAX_TTL;
160         int maxConnectionsPerRoute = ConfigurationProperties.DEFAULT_HTTP_MAX_CONNECTIONS_PER_ROUTE;
161 
162         if (connMgrConfig == null) {
163             registryBuilder.register("https", SSLConnectionSocketFactory.getSystemSocketFactory());
164         } else {
165             // config present: use provided, if any, or create (depending on httpsSecurityMode)
166             connectionMaxTtlSeconds = connMgrConfig.connectionMaxTtlSeconds;
167             maxConnectionsPerRoute = connMgrConfig.maxConnectionsPerRoute;
168             SSLSocketFactory sslSocketFactory =
169                     connMgrConfig.context != null ? connMgrConfig.context.getSocketFactory() : null;
170             HostnameVerifier hostnameVerifier = connMgrConfig.verifier;
171             if (ConfigurationProperties.HTTPS_SECURITY_MODE_DEFAULT.equals(connMgrConfig.httpsSecurityMode)) {
172                 if (sslSocketFactory == null) {
173                     sslSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
174                 }
175                 if (hostnameVerifier == null) {
176                     hostnameVerifier = SSLConnectionSocketFactory.getDefaultHostnameVerifier();
177                 }
178             } else if (ConfigurationProperties.HTTPS_SECURITY_MODE_INSECURE.equals(connMgrConfig.httpsSecurityMode)) {
179                 if (sslSocketFactory == null) {
180                     try {
181                         sslSocketFactory = new SSLContextBuilder()
182                                 .loadTrustMaterial(null, (chain, auth) -> true)
183                                 .build()
184                                 .getSocketFactory();
185                     } catch (Exception e) {
186                         throw new SSLInitializationException(
187                                 "Could not configure '" + connMgrConfig.httpsSecurityMode + "' HTTPS security mode", e);
188                     }
189                 }
190                 if (hostnameVerifier == null) {
191                     hostnameVerifier = NoopHostnameVerifier.INSTANCE;
192                 }
193             } else {
194                 throw new IllegalArgumentException(
195                         "Unsupported '" + connMgrConfig.httpsSecurityMode + "' HTTPS security mode.");
196             }
197 
198             registryBuilder.register(
199                     "https",
200                     new SSLConnectionSocketFactory(
201                             sslSocketFactory, connMgrConfig.protocols, connMgrConfig.cipherSuites, hostnameVerifier));
202         }
203 
204         PoolingHttpClientConnectionManager connMgr = new PoolingHttpClientConnectionManager(
205                 new DefaultHttpClientConnectionOperator(
206                         registryBuilder.build(), DefaultSchemePortResolver.INSTANCE, SystemDefaultDnsResolver.INSTANCE),
207                 ManagedHttpClientConnectionFactory.INSTANCE,
208                 connectionMaxTtlSeconds,
209                 TimeUnit.SECONDS);
210         connMgr.setMaxTotal(maxConnectionsPerRoute * 2);
211         connMgr.setDefaultMaxPerRoute(maxConnectionsPerRoute);
212         return connMgr;
213     }
214 
215     public Object getUserToken(CompoundKey key) {
216         return userTokens.get(key);
217     }
218 
219     public void setUserToken(CompoundKey key, Object userToken) {
220         if (userToken != null) {
221             userTokens.put(key, userToken);
222         } else {
223             userTokens.remove(key);
224         }
225     }
226 
227     public Boolean getExpectContinue(CompoundKey key) {
228         return expectContinues.get(key);
229     }
230 
231     public void setExpectContinue(CompoundKey key, boolean enabled) {
232         expectContinues.put(key, enabled);
233     }
234 }