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