001/* 002 * (C) Copyright 2011-2017 Nuxeo SA (http://nuxeo.com/) and contributors. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the GNU Lesser General Public License 006 * (LGPL) version 2.1 which accompanies this distribution, and is available at 007 * http://www.gnu.org/licenses/lgpl-2.1.html 008 * 009 * This library is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 012 * Lesser General Public License for more details. 013 * 014 * Contributors: 015 * tdelprat 016 * 017 */ 018 019package org.nuxeo.wizard.download; 020 021import java.io.File; 022import java.io.FileInputStream; 023import java.io.FileNotFoundException; 024import java.io.FileReader; 025import java.io.FileWriter; 026import java.io.IOException; 027import java.io.InputStream; 028import java.security.MessageDigest; 029import java.util.ArrayList; 030import java.util.Arrays; 031import java.util.Collections; 032import java.util.List; 033import java.util.Properties; 034import java.util.concurrent.CopyOnWriteArrayList; 035import java.util.concurrent.LinkedBlockingQueue; 036import java.util.concurrent.ThreadFactory; 037import java.util.concurrent.ThreadPoolExecutor; 038import java.util.concurrent.TimeUnit; 039import java.util.concurrent.atomic.AtomicInteger; 040 041import org.apache.commons.lang.StringUtils; 042import org.apache.commons.logging.Log; 043import org.apache.commons.logging.LogFactory; 044import org.apache.http.Header; 045import org.apache.http.HttpHost; 046import org.apache.http.HttpResponse; 047import org.apache.http.auth.AuthScope; 048import org.apache.http.auth.NTCredentials; 049import org.apache.http.auth.UsernamePasswordCredentials; 050import org.apache.http.client.methods.HttpGet; 051import org.apache.http.conn.params.ConnRoutePNames; 052import org.apache.http.conn.scheme.PlainSocketFactory; 053import org.apache.http.conn.scheme.Scheme; 054import org.apache.http.conn.scheme.SchemeRegistry; 055import org.apache.http.conn.ssl.SSLSocketFactory; 056import org.apache.http.impl.client.DefaultHttpClient; 057import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; 058import org.apache.http.params.BasicHttpParams; 059import org.apache.http.params.HttpParams; 060import org.apache.http.params.HttpProtocolParams; 061import org.apache.http.util.EntityUtils; 062import org.nuxeo.common.utils.FileUtils; 063import org.nuxeo.launcher.config.ConfigurationGenerator; 064 065/** 066 * @author Tiry (tdelprat@nuxeo.com) 067 */ 068public class PackageDownloader { 069 070 protected final static Log log = LogFactory.getLog(PackageDownloader.class); 071 072 public static final String PACKAGES_XML = "packages.xml"; 073 074 public static final String PACKAGES_DEFAULT_SELECTION = "packages-default-selection.properties"; 075 076 public static final String PACKAGES_DEFAULT_SELECTION_PRESETS = "preset"; 077 078 public static final String PACKAGES_DEFAULT_SELECTION_PACKAGES = "packages"; 079 080 protected static final int NB_DOWNLOAD_THREADS = 3; 081 082 protected static final int NB_CHECK_THREADS = 1; 083 084 protected static final int QUEUESIZE = 20; 085 086 public static final String BASE_URL_KEY = "nuxeo.wizard.packages.url"; 087 088 public static final String DEFAULT_BASE_URL = "http://cdn.nuxeo.com/"; // nuxeo-XXX/mp 089 090 protected CopyOnWriteArrayList<PendingDownload> pendingDownloads = new CopyOnWriteArrayList<>(); 091 092 protected static PackageDownloader instance; 093 094 protected DefaultHttpClient httpClient; 095 096 protected Boolean canReachServer = null; 097 098 protected DownloadablePackageOptions downloadOptions; 099 100 protected static final String DIGEST = "MD5"; 101 102 protected static final int DIGEST_CHUNK = 1024 * 100; 103 104 boolean downloadStarted = false; 105 106 protected String lastSelectionDigest; 107 108 protected final AtomicInteger dwThreadCount = new AtomicInteger(0); 109 110 protected final AtomicInteger checkThreadCount = new AtomicInteger(0); 111 112 protected String baseUrl; 113 114 protected ConfigurationGenerator configurationGenerator = null; 115 116 protected ConfigurationGenerator getConfig() { 117 if (configurationGenerator == null) { 118 configurationGenerator = new ConfigurationGenerator(); 119 configurationGenerator.init(); 120 } 121 return configurationGenerator; 122 } 123 124 protected String getBaseUrl() { 125 if (baseUrl == null) { 126 String base = getConfig().getUserConfig().getProperty(BASE_URL_KEY, ""); 127 if ("".equals(base)) { 128 base = DEFAULT_BASE_URL + "nuxeo-" 129 + getConfig().getUserConfig().getProperty("org.nuxeo.ecm.product.version") + "/mp/"; 130 } 131 if (!base.endsWith("/")) { 132 base = base + "/"; 133 } 134 baseUrl = base; 135 } 136 return baseUrl; 137 } 138 139 protected ThreadPoolExecutor download_tpe = new ThreadPoolExecutor(NB_DOWNLOAD_THREADS, NB_DOWNLOAD_THREADS, 10L, 140 TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(QUEUESIZE), new ThreadFactory() { 141 @Override 142 public Thread newThread(Runnable r) { 143 Thread t = new Thread(r); 144 t.setDaemon(true); 145 t.setName("DownloaderThread-" + dwThreadCount.incrementAndGet()); 146 return t; 147 } 148 }); 149 150 protected ThreadPoolExecutor check_tpe = new ThreadPoolExecutor(NB_CHECK_THREADS, NB_CHECK_THREADS, 10L, 151 TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(QUEUESIZE), new ThreadFactory() { 152 @Override 153 public Thread newThread(Runnable r) { 154 Thread t = new Thread(r); 155 t.setDaemon(true); 156 t.setName("MD5CheckThread-" + checkThreadCount.incrementAndGet()); 157 return t; 158 } 159 }); 160 161 protected PackageDownloader() { 162 SchemeRegistry registry = new SchemeRegistry(); 163 registry.register(new Scheme("http", 80, PlainSocketFactory.getSocketFactory())); 164 registry.register(new Scheme("https", 443, SSLSocketFactory.getSocketFactory())); 165 HttpParams httpParams = new BasicHttpParams(); 166 HttpProtocolParams.setUseExpectContinue(httpParams, false); 167 ThreadSafeClientConnManager cm = new ThreadSafeClientConnManager(registry); 168 cm.setMaxTotal(NB_DOWNLOAD_THREADS); 169 cm.setDefaultMaxPerRoute(NB_DOWNLOAD_THREADS); 170 httpClient = new DefaultHttpClient(cm, httpParams); 171 } 172 173 public synchronized static PackageDownloader instance() { 174 if (instance == null) { 175 instance = new PackageDownloader(); 176 instance.download_tpe.prestartAllCoreThreads(); 177 instance.check_tpe.prestartAllCoreThreads(); 178 } 179 return instance; 180 } 181 182 public static void reset() { 183 if (instance != null) { 184 instance.shutdown(); 185 instance = null; 186 } 187 } 188 189 public void setProxy(String proxy, int port, String login, String password, String NTLMHost, String NTLMDomain) { 190 if (proxy != null) { 191 HttpHost proxyHost = new HttpHost(proxy, port); 192 httpClient.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxyHost); 193 if (login != null) { 194 if (NTLMHost != null && !NTLMHost.trim().isEmpty()) { 195 NTCredentials ntlmCredentials = new NTCredentials(login, password, NTLMHost, NTLMDomain); 196 httpClient.getCredentialsProvider().setCredentials(new AuthScope(proxy, port), ntlmCredentials); 197 } else { 198 httpClient.getCredentialsProvider().setCredentials(new AuthScope(proxy, port), 199 new UsernamePasswordCredentials(login, password)); 200 } 201 } else { 202 httpClient.getCredentialsProvider().clear(); 203 } 204 } else { 205 httpClient.getParams().removeParameter(ConnRoutePNames.DEFAULT_PROXY); 206 httpClient.getCredentialsProvider().clear(); 207 } 208 } 209 210 protected String getSelectionDigest(List<String> ids) { 211 ArrayList<String> lst = new ArrayList<>(ids); 212 Collections.sort(lst); 213 StringBuffer sb = new StringBuffer(); 214 for (String item : lst) { 215 sb.append(item); 216 sb.append(":"); 217 } 218 return sb.toString(); 219 } 220 221 public void selectOptions(List<String> ids) { 222 String newSelectionDigest = getSelectionDigest(ids); 223 if (lastSelectionDigest != null) { 224 if (lastSelectionDigest.equals(newSelectionDigest)) { 225 return; 226 } 227 } 228 getPackageOptions().select(ids); 229 downloadStarted = false; 230 lastSelectionDigest = newSelectionDigest; 231 } 232 233 protected File getDownloadDirectory() { 234 File mpDir = getConfig().getDistributionMPDir(); 235 if (!mpDir.exists()) { 236 mpDir.mkdirs(); 237 } 238 return mpDir; 239 } 240 241 public boolean canReachServer() { 242 if (canReachServer == null) { 243 HttpGet ping = new HttpGet(getBaseUrl() + PACKAGES_XML); 244 try { 245 HttpResponse response = httpClient.execute(ping); 246 if (response.getStatusLine().getStatusCode() == 200) { 247 canReachServer = true; 248 } else { 249 log.info("Unable to ping server -> status code :" + response.getStatusLine().getStatusCode() + " (" 250 + response.getStatusLine().getReasonPhrase() + ")"); 251 canReachServer = false; 252 } 253 } catch (Exception e) { 254 log.info("Unable to ping remote server " + e.getMessage()); 255 log.debug("Unable to ping remote server", e); 256 canReachServer = false; 257 } 258 } 259 return canReachServer; 260 } 261 262 public DownloadablePackageOptions getPackageOptions() { 263 if (downloadOptions == null) { 264 File packageFile = null; 265 if (canReachServer()) { 266 packageFile = getRemotePackagesDescriptor(); 267 } 268 if (packageFile == null) { 269 packageFile = getLocalPackagesDescriptor(); 270 if (packageFile == null) { 271 log.warn("Unable to find local copy of packages.xml"); 272 } else { 273 log.info("Wizard will use the local copy of packages.xml."); 274 } 275 } 276 if (packageFile != null) { 277 try { 278 downloadOptions = DownloadDescriptorParser.parsePackages(new FileInputStream(packageFile)); 279 280 // manage init from presets if available 281 Properties defaultSelection = getDefaultPackageSelection(); 282 if (defaultSelection != null) { 283 String presetId = defaultSelection.getProperty(PACKAGES_DEFAULT_SELECTION_PRESETS, null); 284 if (presetId != null && !presetId.isEmpty()) { 285 for (Preset preset : downloadOptions.getPresets()) { 286 if (preset.getId().equals(presetId)) { 287 List<String> pkgIds = Arrays.asList(preset.getPkgs()); 288 downloadOptions.select(pkgIds); 289 break; 290 } 291 } 292 } else { 293 String pkgIdsList = defaultSelection.getProperty(PACKAGES_DEFAULT_SELECTION_PACKAGES, null); 294 if (pkgIdsList != null && !pkgIdsList.isEmpty()) { 295 String[] ids = pkgIdsList.split(","); 296 List<String> pkgIds = Arrays.asList(ids); 297 downloadOptions.select(pkgIds); 298 } 299 } 300 } 301 } catch (FileNotFoundException e) { 302 log.error("Unable to read packages.xml", e); 303 } 304 } 305 } 306 return downloadOptions; 307 } 308 309 protected File getRemotePackagesDescriptor() { 310 File desc = null; 311 HttpGet ping = new HttpGet(getBaseUrl() + PACKAGES_XML); 312 try { 313 HttpResponse response = httpClient.execute(ping); 314 if (response.getStatusLine().getStatusCode() == 200) { 315 desc = new File(getDownloadDirectory(), PACKAGES_XML); 316 FileUtils.copyToFile(response.getEntity().getContent(), desc); 317 } else { 318 log.warn("Unable to download remote packages.xml, status code :" 319 + response.getStatusLine().getStatusCode() + " (" + response.getStatusLine().getReasonPhrase() 320 + ")"); 321 return null; 322 } 323 } catch (Exception e) { 324 log.warn("Unable to reach remote packages.xml", e); 325 return null; 326 } 327 return desc; 328 } 329 330 protected Properties getDefaultPackageSelection() { 331 File desc = new File(getDownloadDirectory(), PACKAGES_DEFAULT_SELECTION); 332 if (desc.exists()) { 333 try { 334 Properties props = new Properties(); 335 props.load(new FileReader(desc)); 336 return props; 337 } catch (IOException e) { 338 log.warn("Unable to load presets", e); 339 } 340 } 341 return null; 342 } 343 344 protected void saveSelectedPackages(List<DownloadPackage> pkgs) { 345 File desc = new File(getDownloadDirectory(), PACKAGES_DEFAULT_SELECTION); 346 Properties props = new Properties(); 347 StringBuffer sb = new StringBuffer(); 348 for (int i = 0; i < pkgs.size(); i++) { 349 if (i > 0) { 350 sb.append(","); 351 } 352 sb.append(pkgs.get(i).getId()); 353 } 354 props.put(PACKAGES_DEFAULT_SELECTION_PACKAGES, sb.toString()); 355 try { 356 props.store(new FileWriter(desc), "Saved from Nuxeo SetupWizard"); 357 } catch (IOException e) { 358 log.error("Unable to save package selection", e); 359 } 360 } 361 362 protected File getLocalPackagesDescriptor() { 363 File desc = new File(getDownloadDirectory(), PACKAGES_XML); 364 if (desc.exists()) { 365 return desc; 366 } 367 return null; 368 } 369 370 public List<DownloadPackage> getSelectedPackages() { 371 List<DownloadPackage> pkgs = getPackageOptions().getPkg4Install(); 372 File[] listFiles = getDownloadDirectory().listFiles(); 373 for (DownloadPackage pkg : pkgs) { 374 for (File file : listFiles) { 375 if (file.getName().equals(pkg.getMd5())) { 376 // recheck md5 ??? 377 pkg.setLocalFile(file); 378 } 379 } 380 needToDownload(pkg); 381 } 382 return pkgs; 383 } 384 385 public void scheduleDownloadedPackagesForInstallation(String installationFilePath) throws IOException { 386 List<String> fileEntries = new ArrayList<>(); 387 fileEntries.add("init"); 388 389 List<DownloadPackage> pkgs = downloadOptions.getPkg4Install(); 390 List<String> pkgInstallIds = new ArrayList<>(); 391 for (DownloadPackage pkg : pkgs) { 392 if (pkg.isVirtual()) { 393 log.debug("No install for virtual package: " + pkg.getId()); 394 } else if (pkg.isAlreadyInLocal() || StringUtils.isBlank(pkg.getFilename())) { 395 // Blank filename means later downloaded 396 fileEntries.add("install " + pkg.getId()); 397 pkgInstallIds.add(pkg.getId()); 398 } else { 399 for (PendingDownload download : pendingDownloads) { 400 if (download.getPkg().equals(pkg)) { 401 if (download.getStatus() == PendingDownloadStatus.VERIFIED) { 402 File file = download.getDowloadingFile(); 403 fileEntries.add("add file:" + file.getAbsolutePath()); 404 fileEntries.add("install " + pkg.getId()); 405 pkgInstallIds.add(pkg.getId()); 406 } else { 407 log.error("One selected package has not been downloaded : " + pkg.getId()); 408 } 409 } 410 } 411 } 412 } 413 414 File installLog = new File(installationFilePath); 415 if (fileEntries.size() > 0) { 416 if (!installLog.exists()) { 417 File parent = installLog.getParentFile(); 418 if (!parent.exists()) { 419 parent.mkdirs(); 420 } 421 installLog.createNewFile(); 422 } 423 FileUtils.writeLines(installLog, fileEntries); 424 } else { 425 // Should not happen as the file always has "init" 426 if (installLog.exists()) { 427 installLog.delete(); 428 } 429 } 430 431 // Save presets 432 saveSelectedPackages(pkgs); 433 } 434 435 public List<PendingDownload> getPendingDownloads() { 436 return pendingDownloads; 437 } 438 439 public void reStartDownload(String id) { 440 for (PendingDownload pending : pendingDownloads) { 441 if (pending.getPkg().getId().equals(id)) { 442 if (Arrays.asList(PendingDownloadStatus.CORRUPTED, PendingDownloadStatus.ABORTED).contains( 443 pending.getStatus())) { 444 pendingDownloads.remove(pending); 445 startDownloadPackage(pending.getPkg()); 446 } 447 break; 448 } 449 } 450 } 451 452 public void startDownload() { 453 startDownload(downloadOptions.getPkg4Install()); 454 } 455 456 public void startDownload(List<DownloadPackage> pkgs) { 457 downloadStarted = true; 458 for (DownloadPackage pkg : pkgs) { 459 if (needToDownload(pkg)) { 460 startDownloadPackage(pkg); 461 } 462 } 463 } 464 465 protected boolean needToDownload(DownloadPackage pkg) { 466 return !pkg.isVirtual() && !pkg.isLaterDownload() && !pkg.isAlreadyInLocal(); 467 } 468 469 protected void startDownloadPackage(final DownloadPackage pkg) { 470 final PendingDownload download = new PendingDownload(pkg); 471 if (pendingDownloads.addIfAbsent(download)) { 472 Runnable downloadRunner = new Runnable() { 473 474 @Override 475 public void run() { 476 log.info("Starting download on Thread " + Thread.currentThread().getName()); 477 download.setStatus(PendingDownloadStatus.INPROGRESS); 478 String url = pkg.getDownloadUrl(); 479 if (!url.startsWith("http")) { 480 url = getBaseUrl() + url; 481 } 482 File filePkg = null; 483 HttpGet dw = new HttpGet(url); 484 try { 485 HttpResponse response = httpClient.execute(dw); 486 if (response.getStatusLine().getStatusCode() == 200) { 487 filePkg = new File(getDownloadDirectory(), pkg.filename); 488 Header clh = response.getFirstHeader("Content-Length"); 489 if (clh != null) { 490 long filesize = Long.parseLong(clh.getValue()); 491 download.setFile(filesize, filePkg); 492 } 493 FileUtils.copyToFile(response.getEntity().getContent(), filePkg); 494 download.setStatus(PendingDownloadStatus.COMPLETED); 495 } else if (response.getStatusLine().getStatusCode() == 404) { 496 log.error("Package " + pkg.filename + " not found :" + url); 497 download.setStatus(PendingDownloadStatus.MISSING); 498 EntityUtils.consume(response.getEntity()); 499 dw.abort(); 500 return; 501 } else { 502 log.error("Received StatusCode " + response.getStatusLine().getStatusCode()); 503 download.setStatus(PendingDownloadStatus.ABORTED); 504 EntityUtils.consume(response.getEntity()); 505 dw.abort(); 506 return; 507 } 508 } catch (Exception e) { 509 download.setStatus(PendingDownloadStatus.ABORTED); 510 log.error("Error during download", e); 511 return; 512 } 513 checkPackage(download); 514 } 515 }; 516 download_tpe.execute(downloadRunner); 517 } 518 } 519 520 protected void checkPackage(final PendingDownload download) { 521 final File filePkg = download.getDowloadingFile(); 522 Runnable checkRunner = new Runnable() { 523 @Override 524 public void run() { 525 download.setStatus(PendingDownloadStatus.VERIFICATION); 526 String expectedDigest = download.getPkg().getMd5(); 527 String digest = getDigest(filePkg); 528 if (digest == null || (expectedDigest != null && !expectedDigest.equals(digest))) { 529 download.setStatus(PendingDownloadStatus.CORRUPTED); 530 log.error("Digest check failed: expected=" + expectedDigest + " computed=" + digest); 531 return; 532 } 533 File newFile = new File(getDownloadDirectory(), digest); 534 filePkg.renameTo(newFile); 535 download.setStatus(PendingDownloadStatus.VERIFIED); 536 download.setFile(newFile.length(), newFile); 537 } 538 }; 539 check_tpe.execute(checkRunner); 540 } 541 542 protected String getDigest(File file) { 543 try { 544 MessageDigest md = MessageDigest.getInstance(DIGEST); 545 byte[] buffer = new byte[DIGEST_CHUNK]; 546 InputStream stream = new FileInputStream(file); 547 int bytesRead = -1; 548 while ((bytesRead = stream.read(buffer)) >= 0) { 549 md.update(buffer, 0, bytesRead); 550 } 551 stream.close(); 552 byte[] b = md.digest(); 553 return md5ToHex(b); 554 } catch (Exception e) { 555 log.error("Error while computing Digest ", e); 556 return null; 557 } 558 } 559 560 protected static String md5ToHex(byte[] hash) { 561 StringBuilder hexString = new StringBuilder(); 562 for (byte b : hash) { 563 String hex = Integer.toHexString(0xFF & b); 564 if (hex.length() == 1) { 565 hexString.append('0'); 566 } 567 hexString.append(hex); 568 } 569 return hexString.toString(); 570 } 571 572 public boolean isDownloadStarted() { 573 return downloadStarted; 574 } 575 576 public boolean isDownloadCompleted() { 577 if (!isDownloadStarted()) { 578 return false; 579 } 580 for (PendingDownload download : pendingDownloads) { 581 if (download.getStatus().getValue() < PendingDownloadStatus.VERIFIED.getValue()) { 582 return false; 583 } 584 } 585 return true; 586 } 587 588 public boolean isDownloadInProgress() { 589 if (!isDownloadStarted()) { 590 return false; 591 } 592 if (isDownloadCompleted()) { 593 return false; 594 } 595 int nbInProgress = 0; 596 for (PendingDownload download : pendingDownloads) { 597 if (download.getStatus().getValue() < PendingDownloadStatus.VERIFIED.getValue() 598 && download.getStatus().getValue() >= PendingDownloadStatus.PENDING.getValue()) { 599 nbInProgress++; 600 } 601 } 602 return nbInProgress > 0; 603 } 604 605 public void shutdown() { 606 if (httpClient != null) { 607 httpClient.getConnectionManager().shutdown(); 608 } 609 download_tpe.shutdownNow(); 610 check_tpe.shutdownNow(); 611 } 612 613}