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