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.auth.AuthScope;
40  import org.apache.http.auth.AuthState;
41  import org.apache.http.auth.Credentials;
42  import org.apache.http.auth.UsernamePasswordCredentials;
43  import org.apache.http.client.CredentialsProvider;
44  import org.apache.http.client.methods.HttpRequestBase;
45  import org.apache.http.client.protocol.HttpClientContext;
46  import org.apache.http.conn.ssl.NoopHostnameVerifier;
47  import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
48  import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
49  import org.apache.http.impl.auth.BasicScheme;
50  import org.apache.http.impl.client.BasicCredentialsProvider;
51  import org.apache.http.impl.client.CloseableHttpClient;
52  import org.apache.http.impl.client.HttpClientBuilder;
53  import org.apache.http.impl.client.HttpClients;
54  import org.apache.http.protocol.HttpContext;
55  import org.apache.http.ssl.SSLContextBuilder;
56  import org.jdom2.Document;
57  import org.jetbrains.annotations.NotNull;
58  import org.jetbrains.annotations.Nullable;
59  import org.json.JSONObject;
60  import org.slf4j.Logger;
61  import org.slf4j.LoggerFactory;
62  
63  import io.wcm.tooling.commons.packmgr.httpaction.BundleStatus;
64  import io.wcm.tooling.commons.packmgr.httpaction.BundleStatusCall;
65  import io.wcm.tooling.commons.packmgr.httpaction.HttpCall;
66  import io.wcm.tooling.commons.packmgr.httpaction.PackageManagerHtmlCall;
67  import io.wcm.tooling.commons.packmgr.httpaction.PackageManagerHtmlMessageCall;
68  import io.wcm.tooling.commons.packmgr.httpaction.PackageManagerInstallStatus;
69  import io.wcm.tooling.commons.packmgr.httpaction.PackageManagerInstallStatusCall;
70  import io.wcm.tooling.commons.packmgr.httpaction.PackageManagerJsonCall;
71  import io.wcm.tooling.commons.packmgr.httpaction.PackageManagerStatusCall;
72  import io.wcm.tooling.commons.packmgr.httpaction.PackageManagerXmlCall;
73  import io.wcm.tooling.commons.packmgr.httpaction.SystemReadyStatus;
74  import io.wcm.tooling.commons.packmgr.httpaction.SystemReadyStatusCall;
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         // keep reusing connections to a minimum - may conflict when instance is restarting and responds in unexpected manner
108         .setKeepAliveStrategy((response, context) -> 1)
109         .addInterceptorFirst(new HttpRequestInterceptor() {
110           @Override
111           public void process(HttpRequest request, HttpContext context) throws HttpException, IOException {
112             Credentials credentials = (Credentials)context.getAttribute(HTTP_CONTEXT_ATTRIBUTE_PREEMPTIVE_AUTHENTICATION_CREDS);
113             if (credentials != null) {
114               // enable preemptive authentication
115               AuthState authState = (AuthState)context.getAttribute(HttpClientContext.TARGET_AUTH_STATE);
116               authState.update(new BasicScheme(), credentials);
117             }
118             String oauth2AccessToken = (String)context.getAttribute(HTTP_CONTEXT_ATTRIBUTE_OAUTH2_ACCESS_TOKEN);
119             if (oauth2AccessToken != null) {
120               // send OAuth 2 bearer token
121               request.setHeader("Authorization", "Bearer " + oauth2AccessToken);
122             }
123           }
124         });
125 
126     // relaxed SSL check
127     if (props.isRelaxedSSLCheck()) {
128       try {
129         SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustSelfSignedStrategy()).build();
130         SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext, new NoopHostnameVerifier());
131         httpClientBuilder.setSSLSocketFactory(sslsf);
132       }
133       catch (KeyManagementException | KeyStoreException | NoSuchAlgorithmException ex) {
134         throw new PackageManagerException("Could not set relaxedSSLCheck", ex);
135       }
136     }
137 
138     // proxy support
139     Proxy proxy = getProxyForUrl(props.getPackageManagerUrl());
140     if (proxy != null) {
141       httpClientBuilder.setProxy(new HttpHost(proxy.getHost(), proxy.getPort(), proxy.getProtocol()));
142     }
143 
144     return httpClientBuilder.build();
145   }
146 
147   /**
148    * Set up http client context with credentials for CRX package manager.
149    * @return Http client context
150    */
151   public @NotNull HttpClientContext getPackageManagerHttpClientContext() {
152     return getHttpClientContext(props.getPackageManagerUrl(),
153         props.getUserId(), props.getPassword(), props.getOAuth2AccessToken());
154   }
155 
156   /**
157    * Set up http client context with credentials for Felix console.
158    * @return Http client context. May be null of bundle status URL is set to "-".
159    */
160   public @Nullable HttpClientContext getConsoleHttpClientContext() {
161     String bundleStatusUrl = props.getBundleStatusUrl();
162     if (bundleStatusUrl == null) {
163       return null;
164     }
165     return getHttpClientContext(bundleStatusUrl,
166         props.getConsoleUserId(), props.getConsolePassword(), props.getConsoleOAuth2AccessToken());
167   }
168 
169   private @NotNull HttpClientContext getHttpClientContext(String url, String userId, String password, String oauth2AccessToken) {
170     URI uri;
171     try {
172       uri = new URI(url);
173     }
174     catch (URISyntaxException ex) {
175       throw new PackageManagerException("Invalid url: " + url, ex);
176     }
177 
178     final CredentialsProvider credsProvider = new BasicCredentialsProvider();
179     HttpClientContext context = new HttpClientContext();
180     context.setCredentialsProvider(credsProvider);
181 
182     if (StringUtils.isNotBlank(oauth2AccessToken)) {
183       context.setAttribute(HTTP_CONTEXT_ATTRIBUTE_OAUTH2_ACCESS_TOKEN, oauth2AccessToken);
184     }
185     else {
186       // use basic (preemptive) authentication with username/password
187       final AuthScope authScope = new AuthScope(uri.getHost(), uri.getPort());
188       final Credentials credentials = new UsernamePasswordCredentials(userId, password);
189       credsProvider.setCredentials(authScope, credentials);
190       context.setAttribute(HTTP_CONTEXT_ATTRIBUTE_PREEMPTIVE_AUTHENTICATION_CREDS, credentials);
191     }
192 
193     // timeout settings
194     context.setRequestConfig(HttpClientUtil.buildRequestConfig(props));
195 
196     // proxy support
197     Proxy proxy = getProxyForUrl(url);
198     if (proxy != null && proxy.useAuthentication()) {
199       AuthScope proxyAuthScope = new AuthScope(proxy.getHost(), proxy.getPort());
200       Credentials proxyCredentials = new UsernamePasswordCredentials(proxy.getUsername(), proxy.getPassword());
201       credsProvider.setCredentials(proxyAuthScope, proxyCredentials);
202     }
203 
204     return context;
205   }
206 
207   /**
208    * Get proxy for given URL
209    * @param requestUrl Request URL
210    * @return Proxy or null if none matching found
211    */
212   private Proxy getProxyForUrl(String requestUrl) {
213     List<Proxy> proxies = props.getProxies();
214     if (proxies == null || proxies.isEmpty()) {
215       return null;
216     }
217     final URI uri = URI.create(requestUrl);
218     for (Proxy proxy : proxies) {
219       if (!proxy.isNonProxyHost(uri.getHost())) {
220         return proxy;
221       }
222     }
223     return null;
224   }
225 
226 
227   /**
228    * Execute HTTP call with automatic retry as configured for the MOJO.
229    * @param call HTTP call
230    * @param runCount Number of runs this call was already executed
231    */
232   @SuppressWarnings("PMD.GuardLogStatement")
233   private <T> T executeHttpCallWithRetry(HttpCall<T> call, int runCount) {
234     try {
235       return call.execute();
236     }
237     catch (PackageManagerHttpActionException ex) {
238       // retry again if configured so...
239       if (runCount < props.getRetryCount()) {
240         log.warn("ERROR: {}", ex.getMessage());
241         log.debug("HTTP call failed.", ex);
242         log.warn("---------------");
243 
244         StringBuilder msg = new StringBuilder();
245         msg.append("HTTP call failed, try again (").append(runCount + 1).append("/").append(props.getRetryCount()).append(")");
246         if (props.getRetryDelaySec() > 0) {
247           msg.append(" after ").append(props.getRetryDelaySec()).append(" second(s)");
248         }
249         msg.append("...");
250         log.warn(msg.toString());
251         if (props.getRetryDelaySec() > 0) {
252           try {
253             Thread.sleep(props.getRetryDelaySec() * DateUtils.MILLIS_PER_SECOND);
254           }
255           catch (InterruptedException ex1) {
256             // ignore
257           }
258         }
259         return executeHttpCallWithRetry(call, runCount + 1);
260       }
261       else {
262         throw ex;
263       }
264     }
265   }
266 
267   /**
268    * Execute CRX HTTP Package manager method and parse JSON response.
269    * @param httpClient HTTP client
270    * @param context HTTP client context
271    * @param method Get or Post method
272    * @return JSON object
273    */
274   public JSONObject executePackageManagerMethodJson(CloseableHttpClient httpClient, HttpClientContext context, HttpRequestBase method) {
275     PackageManagerJsonCall call = new PackageManagerJsonCall(httpClient, context, method);
276     return executeHttpCallWithRetry(call, 0);
277   }
278 
279   /**
280    * Execute CRX HTTP Package manager method and parse XML response.
281    * @param httpClient HTTP client
282    * @param context HTTP client context
283    * @param method Get or Post method
284    * @return XML document
285    */
286   public Document executePackageManagerMethodXml(CloseableHttpClient httpClient, HttpClientContext context, HttpRequestBase method) {
287     PackageManagerXmlCall call = new PackageManagerXmlCall(httpClient, context, method);
288     return executeHttpCallWithRetry(call, 0);
289   }
290 
291   /**
292    * Execute CRX HTTP Package manager method and get HTML response.
293    * @param httpClient HTTP client
294    * @param context HTTP client context
295    * @param method Get or Post method
296    * @return Response from HTML server
297    */
298   public String executePackageManagerMethodHtml(CloseableHttpClient httpClient, HttpClientContext context, HttpRequestBase method) {
299     PackageManagerHtmlCall call = new PackageManagerHtmlCall(httpClient, context, method);
300     return executeHttpCallWithRetry(call, 0);
301   }
302 
303   /**
304    * Execute CRX HTTP Package manager method and output HTML response.
305    * @param httpClient HTTP client
306    * @param context HTTP client context
307    * @param method Get or Post method
308    */
309   public void executePackageManagerMethodHtmlOutputResponse(CloseableHttpClient httpClient, HttpClientContext context, HttpRequestBase method) {
310     PackageManagerHtmlMessageCall call = new PackageManagerHtmlMessageCall(httpClient, context, method, props);
311     executeHttpCallWithRetry(call, 0);
312   }
313 
314   /**
315    * Execute CRX HTTP Package manager method and checks response status. If the response status is not 200 the call
316    * fails (after retrying).
317    * @param httpClient HTTP client
318    * @param context HTTP client context
319    * @param method Get or Post method
320    */
321   public void executePackageManagerMethodStatus(CloseableHttpClient httpClient, HttpClientContext context, HttpRequestBase method) {
322     PackageManagerStatusCall call = new PackageManagerStatusCall(httpClient, context, method);
323     executeHttpCallWithRetry(call, 0);
324   }
325 
326   /**
327    * Wait for bundles to become active.
328    * @param httpClient HTTP client
329    * @param context HTTP client context
330    */
331   @SuppressWarnings("PMD.GuardLogStatement")
332   public void waitForBundlesActivation(CloseableHttpClient httpClient, HttpClientContext context) {
333     if (StringUtils.isBlank(props.getBundleStatusUrl())) {
334       log.debug("Skipping check for bundle activation state because no bundleStatusURL is defined.");
335       return;
336     }
337 
338     final int WAIT_INTERVAL_SEC = 3;
339     final long CHECK_RETRY_COUNT = props.getBundleStatusWaitLimitSec() / WAIT_INTERVAL_SEC;
340 
341     log.info("Check bundle activation status...");
342     for (int i = 1; i <= CHECK_RETRY_COUNT; i++) {
343       BundleStatusCall call = new BundleStatusCall(httpClient, context, props.getBundleStatusUrl(),
344           props.getBundleStatusWhitelistBundleNames());
345       BundleStatus bundleStatus = executeHttpCallWithRetry(call, 0);
346 
347       boolean instanceReady = true;
348 
349       // check if bundles are still stopping/staring
350       if (!bundleStatus.isAllBundlesRunning()) {
351         log.info("Bundles starting/stopping: {} - wait {} sec (max. {} sec) ...",
352             bundleStatus.getStatusLineCompact(), WAIT_INTERVAL_SEC, props.getBundleStatusWaitLimitSec());
353         sleep(WAIT_INTERVAL_SEC);
354         instanceReady = false;
355       }
356 
357       // check if any of the blacklisted bundles is still present
358       if (instanceReady) {
359         for (Pattern blacklistBundleNamePattern : props.getBundleStatusBlacklistBundleNames()) {
360           String bundleSymbolicName = bundleStatus.getMatchingBundle(blacklistBundleNamePattern);
361           if (bundleSymbolicName != null) {
362             log.info("Bundle '{}' is still deployed - wait {} sec (max. {} sec) ...",
363                 bundleSymbolicName, WAIT_INTERVAL_SEC, props.getBundleStatusWaitLimitSec());
364             sleep(WAIT_INTERVAL_SEC);
365             instanceReady = false;
366             break;
367           }
368         }
369       }
370 
371       // instance is ready
372       if (instanceReady) {
373         break;
374       }
375     }
376   }
377 
378   /**
379    * Wait for system ready status to get OK.
380    * @param httpClient HTTP client
381    * @param context HTTP client context
382    */
383   @SuppressWarnings("PMD.GuardLogStatement")
384   public void waitForSystemReady(CloseableHttpClient httpClient, HttpClientContext context) {
385     if (StringUtils.isBlank(props.getSystemReadyUrl())) {
386       log.debug("Skipping check for system ready because no systemReadyURL is defined.");
387       return;
388     }
389 
390     final int WAIT_INTERVAL_SEC = 3;
391     final long CHECK_RETRY_COUNT = props.getSystemReadyWaitLimitSec() / WAIT_INTERVAL_SEC;
392 
393     log.info("Check system ready status...");
394     boolean systemReady = true;
395     for (int i = 1; i <= CHECK_RETRY_COUNT; i++) {
396       boolean lastTry = (i == CHECK_RETRY_COUNT);
397 
398       SystemReadyStatusCall call = new SystemReadyStatusCall(httpClient, context, props.getSystemReadyUrl());
399       SystemReadyStatus systemReadyStatus = executeHttpCallWithRetry(call, 0);
400 
401       // check if bundles are still stopping/staring
402       if (!systemReadyStatus.isSystemReadyOK()) {
403         if (lastTry) {
404           throw new PackageManagerException("System is NOT ready (" + systemReadyStatus.getOverallResult() + ") - package deployment failed.\n"
405               + systemReadyStatus.getFailureInfoString());
406         }
407         log.warn("System is NOT ready ({}) - wait {} sec (max. {} sec) ...\n{}",
408             systemReadyStatus.getOverallResult(), WAIT_INTERVAL_SEC, props.getSystemReadyWaitLimitSec(),
409             systemReadyStatus.getFailureInfoString());
410         sleep(WAIT_INTERVAL_SEC);
411         systemReady = false;
412       }
413 
414       // instance is ready
415       if (systemReady) {
416         break;
417       }
418     }
419   }
420 
421   /**
422    * Wait for package manager install status to become finished.
423    * @param httpClient HTTP client
424    * @param context HTTP client context
425    */
426   @SuppressWarnings("PMD.GuardLogStatement")
427   public void waitForPackageManagerInstallStatusFinished(CloseableHttpClient httpClient, HttpClientContext context) {
428     if (StringUtils.isBlank(props.getPackageManagerInstallStatusURL())) {
429       log.debug("Skipping check for package manager install state because no packageManagerInstallStatusURL is defined.");
430       return;
431     }
432 
433     final int WAIT_INTERVAL_SEC = 3;
434     final long CHECK_RETRY_COUNT = props.getPackageManagerInstallStatusWaitLimitSec() / WAIT_INTERVAL_SEC;
435 
436     log.info("Check package manager installation status...");
437     for (int i = 1; i <= CHECK_RETRY_COUNT; i++) {
438       PackageManagerInstallStatusCall call = new PackageManagerInstallStatusCall(httpClient, context,
439           props.getPackageManagerInstallStatusURL());
440       PackageManagerInstallStatus packageManagerStatus = executeHttpCallWithRetry(call, 0);
441 
442       boolean instanceReady = true;
443 
444       // check if package manager is still installing packages
445       if (!packageManagerStatus.isFinished()) {
446         log.info("Packager manager not ready: {} packages left for installation - wait {} sec (max. {} sec) ...",
447             packageManagerStatus.getItemCount(), WAIT_INTERVAL_SEC, props.getPackageManagerInstallStatusWaitLimitSec());
448         sleep(WAIT_INTERVAL_SEC);
449         instanceReady = false;
450       }
451 
452       // instance is ready
453       if (instanceReady) {
454         break;
455       }
456     }
457   }
458 
459   private void sleep(int sec) {
460     try {
461       Thread.sleep(sec * DateUtils.MILLIS_PER_SECOND);
462     }
463     catch (InterruptedException e) {
464       // ignore
465     }
466   }
467 
468 }