View Javadoc
1   /*
2    * #%L
3    * wcm.io
4    * %%
5    * Copyright (C) 2014 wcm.io
6    * %%
7    * Licensed under the Apache License, Version 2.0 (the "License");
8    * you may not use this file except in compliance with the License.
9    * You may obtain a copy of the License at
10   *
11   *      http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
18   * #L%
19   */
20  package io.wcm.tooling.commons.packmgr;
21  
22  import java.io.IOException;
23  import java.net.URI;
24  import java.net.URISyntaxException;
25  import java.security.KeyManagementException;
26  import java.security.KeyStoreException;
27  import java.security.NoSuchAlgorithmException;
28  import java.util.List;
29  import java.util.regex.Pattern;
30  
31  import javax.net.ssl.SSLContext;
32  
33  import org.apache.commons.lang3.StringUtils;
34  import org.apache.commons.lang3.time.DateUtils;
35  import org.apache.http.HttpException;
36  import org.apache.http.HttpHost;
37  import org.apache.http.HttpRequest;
38  import org.apache.http.HttpRequestInterceptor;
39  import org.apache.http.HttpResponse;
40  import org.apache.http.auth.AuthScope;
41  import org.apache.http.auth.AuthState;
42  import org.apache.http.auth.Credentials;
43  import org.apache.http.auth.UsernamePasswordCredentials;
44  import org.apache.http.client.CredentialsProvider;
45  import org.apache.http.client.methods.HttpRequestBase;
46  import org.apache.http.client.protocol.HttpClientContext;
47  import org.apache.http.conn.ConnectionKeepAliveStrategy;
48  import org.apache.http.conn.ssl.NoopHostnameVerifier;
49  import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
50  import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
51  import org.apache.http.impl.auth.BasicScheme;
52  import org.apache.http.impl.client.BasicCredentialsProvider;
53  import org.apache.http.impl.client.CloseableHttpClient;
54  import org.apache.http.impl.client.HttpClientBuilder;
55  import org.apache.http.impl.client.HttpClients;
56  import org.apache.http.protocol.HttpContext;
57  import org.apache.http.ssl.SSLContextBuilder;
58  import org.jdom2.Document;
59  import org.jetbrains.annotations.NotNull;
60  import org.jetbrains.annotations.Nullable;
61  import org.json.JSONObject;
62  import org.slf4j.Logger;
63  import org.slf4j.LoggerFactory;
64  
65  import io.wcm.tooling.commons.packmgr.httpaction.BundleStatus;
66  import io.wcm.tooling.commons.packmgr.httpaction.BundleStatusCall;
67  import io.wcm.tooling.commons.packmgr.httpaction.HttpCall;
68  import io.wcm.tooling.commons.packmgr.httpaction.PackageManagerHtmlCall;
69  import io.wcm.tooling.commons.packmgr.httpaction.PackageManagerHtmlMessageCall;
70  import io.wcm.tooling.commons.packmgr.httpaction.PackageManagerInstallStatus;
71  import io.wcm.tooling.commons.packmgr.httpaction.PackageManagerInstallStatusCall;
72  import io.wcm.tooling.commons.packmgr.httpaction.PackageManagerJsonCall;
73  import io.wcm.tooling.commons.packmgr.httpaction.PackageManagerStatusCall;
74  import io.wcm.tooling.commons.packmgr.httpaction.PackageManagerXmlCall;
75  import io.wcm.tooling.commons.packmgr.util.HttpClientUtil;
76  
77  /**
78   * Common functionality for all mojos.
79   */
80  public final class PackageManagerHelper {
81  
82    /**
83     * Prefix or error message from CRX HTTP interfaces when uploading a package that already exists.
84     */
85    public static final String CRX_PACKAGE_EXISTS_ERROR_MESSAGE_PREFIX = "Package already exists: ";
86  
87    private static final String HTTP_CONTEXT_ATTRIBUTE_PREEMPTIVE_AUTHENTICATION_CREDS = PackageManagerHelper.class.getName() + "_PreemptiveAuthenticationCreds";
88    private static final String HTTP_CONTEXT_ATTRIBUTE_OAUTH2_ACCESS_TOKEN = PackageManagerHelper.class.getName() + "_oauth2AccessToken";
89  
90    private final PackageManagerProperties props;
91  
92    private static final Logger log = LoggerFactory.getLogger(PackageManagerHelper.class);
93  
94    /**
95     * @param props Package manager properties
96     */
97    public PackageManagerHelper(PackageManagerProperties props) {
98      this.props = props;
99    }
100 
101   /**
102    * Get HTTP client to be used for all communications (package manager and Felix console).
103    * @return HTTP client
104    */
105   public @NotNull CloseableHttpClient getHttpClient() {
106     HttpClientBuilder httpClientBuilder = HttpClients.custom()
107         .setKeepAliveStrategy(new ConnectionKeepAliveStrategy() {
108           @Override
109           public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
110             // keep reusing connections to a minimum - may conflict when instance is restarting and responds in unexpected manner
111             return 1;
112           }
113         })
114         .addInterceptorFirst(new HttpRequestInterceptor() {
115           @Override
116           public void process(HttpRequest request, HttpContext context) throws HttpException, IOException {
117             Credentials credentials = (Credentials)context.getAttribute(HTTP_CONTEXT_ATTRIBUTE_PREEMPTIVE_AUTHENTICATION_CREDS);
118             if (credentials != null) {
119               // enable preemptive authentication
120               AuthState authState = (AuthState)context.getAttribute(HttpClientContext.TARGET_AUTH_STATE);
121               authState.update(new BasicScheme(), credentials);
122             }
123             String oauth2AccessToken = (String)context.getAttribute(HTTP_CONTEXT_ATTRIBUTE_OAUTH2_ACCESS_TOKEN);
124             if (oauth2AccessToken != null) {
125               // send OAuth 2 bearer token
126               request.setHeader("Authorization", "Bearer " + oauth2AccessToken);
127             }
128           }
129         });
130 
131     // relaxed SSL check
132     if (props.isRelaxedSSLCheck()) {
133       try {
134         SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustSelfSignedStrategy()).build();
135         SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext, new NoopHostnameVerifier());
136         httpClientBuilder.setSSLSocketFactory(sslsf);
137       }
138       catch (KeyManagementException | KeyStoreException | NoSuchAlgorithmException ex) {
139         throw new PackageManagerException("Could not set relaxedSSLCheck", ex);
140       }
141     }
142 
143     // proxy support
144     Proxy proxy = getProxyForUrl(props.getPackageManagerUrl());
145     if (proxy != null) {
146       httpClientBuilder.setProxy(new HttpHost(proxy.getHost(), proxy.getPort(), proxy.getProtocol()));
147     }
148 
149     return httpClientBuilder.build();
150   }
151 
152   /**
153    * Set up http client context with credentials for CRX package manager.
154    * @return Http client context
155    */
156   public @NotNull HttpClientContext getPackageManagerHttpClientContext() {
157     return getHttpClientContext(props.getPackageManagerUrl(),
158         props.getUserId(), props.getPassword(), props.getOAuth2AccessToken());
159   }
160 
161   /**
162    * Set up http client context with credentials for Felix console.
163    * @return Http client context. May be null of bundle status URL is set to "-".
164    */
165   public @Nullable HttpClientContext getConsoleHttpClientContext() {
166     String bundleStatusUrl = props.getBundleStatusUrl();
167     if (bundleStatusUrl == null) {
168       return null;
169     }
170     return getHttpClientContext(bundleStatusUrl,
171         props.getConsoleUserId(), props.getConsolePassword(), props.getConsoleOAuth2AccessToken());
172   }
173 
174   private @NotNull HttpClientContext getHttpClientContext(String url, String userId, String password, String oauth2AccessToken) {
175     URI uri;
176     try {
177       uri = new URI(url);
178     }
179     catch (URISyntaxException ex) {
180       throw new PackageManagerException("Invalid url: " + url, ex);
181     }
182 
183     final CredentialsProvider credsProvider = new BasicCredentialsProvider();
184     HttpClientContext context = new HttpClientContext();
185     context.setCredentialsProvider(credsProvider);
186 
187     if (StringUtils.isNotBlank(oauth2AccessToken)) {
188       context.setAttribute(HTTP_CONTEXT_ATTRIBUTE_OAUTH2_ACCESS_TOKEN, oauth2AccessToken);
189     }
190     else {
191       // use basic (preemptive) authentication with username/password
192       final AuthScope authScope = new AuthScope(uri.getHost(), uri.getPort());
193       final Credentials credentials = new UsernamePasswordCredentials(userId, password);
194       credsProvider.setCredentials(authScope, credentials);
195       context.setAttribute(HTTP_CONTEXT_ATTRIBUTE_PREEMPTIVE_AUTHENTICATION_CREDS, credentials);
196     }
197 
198     // timeout settings
199     context.setRequestConfig(HttpClientUtil.buildRequestConfig(props));
200 
201     // proxy support
202     Proxy proxy = getProxyForUrl(url);
203     if (proxy != null && proxy.useAuthentication()) {
204       AuthScope proxyAuthScope = new AuthScope(proxy.getHost(), proxy.getPort());
205       Credentials proxyCredentials = new UsernamePasswordCredentials(proxy.getUsername(), proxy.getPassword());
206       credsProvider.setCredentials(proxyAuthScope, proxyCredentials);
207     }
208 
209     return context;
210   }
211 
212   /**
213    * Get proxy for given URL
214    * @param requestUrl Request URL
215    * @return Proxy or null if none matching found
216    */
217   private Proxy getProxyForUrl(String requestUrl) {
218     List<Proxy> proxies = props.getProxies();
219     if (proxies == null || proxies.isEmpty()) {
220       return null;
221     }
222     final URI uri = URI.create(requestUrl);
223     for (Proxy proxy : proxies) {
224       if (!proxy.isNonProxyHost(uri.getHost())) {
225         return proxy;
226       }
227     }
228     return null;
229   }
230 
231 
232   /**
233    * Execute HTTP call with automatic retry as configured for the MOJO.
234    * @param call HTTP call
235    * @param runCount Number of runs this call was already executed
236    */
237   @SuppressWarnings("PMD.GuardLogStatement")
238   private <T> T executeHttpCallWithRetry(HttpCall<T> call, int runCount) {
239     try {
240       return call.execute();
241     }
242     catch (PackageManagerHttpActionException ex) {
243       // retry again if configured so...
244       if (runCount < props.getRetryCount()) {
245         log.warn("ERROR: {}", ex.getMessage());
246         log.debug("HTTP call failed.", ex);
247         log.warn("---------------");
248 
249         StringBuilder msg = new StringBuilder();
250         msg.append("HTTP call failed, try again (" + (runCount + 1) + "/" + props.getRetryCount() + ")");
251         if (props.getRetryDelaySec() > 0) {
252           msg.append(" after " + props.getRetryDelaySec() + " second(s)");
253         }
254         msg.append("...");
255         log.warn(msg.toString());
256         if (props.getRetryDelaySec() > 0) {
257           try {
258             Thread.sleep(props.getRetryDelaySec() * DateUtils.MILLIS_PER_SECOND);
259           }
260           catch (InterruptedException ex1) {
261             // ignore
262           }
263         }
264         return executeHttpCallWithRetry(call, runCount + 1);
265       }
266       else {
267         throw ex;
268       }
269     }
270   }
271 
272   /**
273    * Execute CRX HTTP Package manager method and parse JSON response.
274    * @param httpClient HTTP client
275    * @param context HTTP client context
276    * @param method Get or Post method
277    * @return JSON object
278    */
279   public JSONObject executePackageManagerMethodJson(CloseableHttpClient httpClient, HttpClientContext context, HttpRequestBase method) {
280     PackageManagerJsonCall call = new PackageManagerJsonCall(httpClient, context, method);
281     return executeHttpCallWithRetry(call, 0);
282   }
283 
284   /**
285    * Execute CRX HTTP Package manager method and parse XML response.
286    * @param httpClient HTTP client
287    * @param context HTTP client context
288    * @param method Get or Post method
289    * @return XML document
290    */
291   public Document executePackageManagerMethodXml(CloseableHttpClient httpClient, HttpClientContext context, HttpRequestBase method) {
292     PackageManagerXmlCall call = new PackageManagerXmlCall(httpClient, context, method);
293     return executeHttpCallWithRetry(call, 0);
294   }
295 
296   /**
297    * Execute CRX HTTP Package manager method and get HTML response.
298    * @param httpClient HTTP client
299    * @param context HTTP client context
300    * @param method Get or Post method
301    * @return Response from HTML server
302    */
303   public String executePackageManagerMethodHtml(CloseableHttpClient httpClient, HttpClientContext context, HttpRequestBase method) {
304     PackageManagerHtmlCall call = new PackageManagerHtmlCall(httpClient, context, method);
305     return executeHttpCallWithRetry(call, 0);
306   }
307 
308   /**
309    * Execute CRX HTTP Package manager method and output HTML response.
310    * @param httpClient HTTP client
311    * @param context HTTP client context
312    * @param method Get or Post method
313    */
314   public void executePackageManagerMethodHtmlOutputResponse(CloseableHttpClient httpClient, HttpClientContext context, HttpRequestBase method) {
315     PackageManagerHtmlMessageCall call = new PackageManagerHtmlMessageCall(httpClient, context, method, props);
316     executeHttpCallWithRetry(call, 0);
317   }
318 
319   /**
320    * Execute CRX HTTP Package manager method and checks response status. If the response status is not 200 the call
321    * fails (after retrying).
322    * @param httpClient HTTP client
323    * @param context HTTP client context
324    * @param method Get or Post method
325    */
326   public void executePackageManagerMethodStatus(CloseableHttpClient httpClient, HttpClientContext context, HttpRequestBase method) {
327     PackageManagerStatusCall call = new PackageManagerStatusCall(httpClient, context, method);
328     executeHttpCallWithRetry(call, 0);
329   }
330 
331   /**
332    * Wait for bundles to become active.
333    * @param httpClient HTTP client
334    * @param context HTTP client context
335    */
336   @SuppressWarnings("PMD.GuardLogStatement")
337   public void waitForBundlesActivation(CloseableHttpClient httpClient, HttpClientContext context) {
338     if (StringUtils.isBlank(props.getBundleStatusUrl())) {
339       log.debug("Skipping check for bundle activation state because no bundleStatusURL is defined.");
340       return;
341     }
342 
343     final int WAIT_INTERVAL_SEC = 3;
344     final long CHECK_RETRY_COUNT = props.getBundleStatusWaitLimitSec() / WAIT_INTERVAL_SEC;
345 
346     log.info("Check bundle activation status...");
347     for (int i = 1; i <= CHECK_RETRY_COUNT; i++) {
348       BundleStatusCall call = new BundleStatusCall(httpClient, context, props.getBundleStatusUrl(),
349           props.getBundleStatusWhitelistBundleNames());
350       BundleStatus bundleStatus = executeHttpCallWithRetry(call, 0);
351 
352       boolean instanceReady = true;
353 
354       // check if bundles are still stopping/staring
355       if (!bundleStatus.isAllBundlesRunning()) {
356         log.info("Bundles starting/stopping: {} - wait {} sec (max. {} sec) ...",
357             bundleStatus.getStatusLineCompact(), WAIT_INTERVAL_SEC, props.getBundleStatusWaitLimitSec());
358         sleep(WAIT_INTERVAL_SEC);
359         instanceReady = false;
360       }
361 
362       // check if any of the blacklisted bundles is still present
363       if (instanceReady) {
364         for (Pattern blacklistBundleNamePattern : props.getBundleStatusBlacklistBundleNames()) {
365           String bundleSymbolicName = bundleStatus.getMatchingBundle(blacklistBundleNamePattern);
366           if (bundleSymbolicName != null) {
367             log.info("Bundle '{}' is still deployed - wait {} sec (max. {} sec) ...",
368                 bundleSymbolicName, WAIT_INTERVAL_SEC, props.getBundleStatusWaitLimitSec());
369             sleep(WAIT_INTERVAL_SEC);
370             instanceReady = false;
371             break;
372           }
373         }
374       }
375 
376       // instance is ready
377       if (instanceReady) {
378         break;
379       }
380     }
381   }
382 
383   /**
384    * Wait for package manager install status to become finished.
385    * @param httpClient HTTP client
386    * @param context HTTP client context
387    */
388   @SuppressWarnings("PMD.GuardLogStatement")
389   public void waitForPackageManagerInstallStatusFinished(CloseableHttpClient httpClient, HttpClientContext context) {
390     if (StringUtils.isBlank(props.getPackageManagerInstallStatusURL())) {
391       log.debug("Skipping check for package manager install state because no packageManagerInstallStatusURL is defined.");
392       return;
393     }
394 
395     final int WAIT_INTERVAL_SEC = 3;
396     final long CHECK_RETRY_COUNT = props.getPackageManagerInstallStatusWaitLimitSec() / WAIT_INTERVAL_SEC;
397 
398     log.info("Check package manager installation status...");
399     for (int i = 1; i <= CHECK_RETRY_COUNT; i++) {
400       PackageManagerInstallStatusCall call = new PackageManagerInstallStatusCall(httpClient, context,
401           props.getPackageManagerInstallStatusURL());
402       PackageManagerInstallStatus packageManagerStatus = executeHttpCallWithRetry(call, 0);
403 
404       boolean instanceReady = true;
405 
406       // check if package manager is still installing packages
407       if (!packageManagerStatus.isFinished()) {
408         log.info("Packager manager not ready: {} packages left for installation - wait {} sec (max. {} sec) ...",
409             packageManagerStatus.getItemCount(), WAIT_INTERVAL_SEC, props.getPackageManagerInstallStatusWaitLimitSec());
410         sleep(WAIT_INTERVAL_SEC);
411         instanceReady = false;
412       }
413 
414       // instance is ready
415       if (instanceReady) {
416         break;
417       }
418     }
419   }
420 
421   private void sleep(int sec) {
422     try {
423       Thread.sleep(sec * DateUtils.MILLIS_PER_SECOND);
424     }
425     catch (InterruptedException e) {
426       // ignore
427     }
428   }
429 
430 }