/***************************************************************************** * Copyright (c) 2006-2013, Cloudsmith Inc. * The code, documentation and other materials contained herein have been * licensed under the Eclipse Public License - v 1.0 by the copyright holder * listed above, as the Initial Contributor under such license. The text of * such license is available at www.eclipse.org. *****************************************************************************/ package org.eclipse.buckminster.maven.internal; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URL; import java.security.DigestOutputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Arrays; import org.eclipse.buckminster.download.DownloadManager; import org.eclipse.buckminster.maven.MavenPlugin; import org.eclipse.buckminster.maven.Messages; import org.eclipse.buckminster.runtime.BuckminsterException; import org.eclipse.buckminster.runtime.IOUtils; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.ecf.core.security.IConnectContext; import org.eclipse.osgi.util.NLS; /** * @author Thomas Hallgren * */ public class LocalCache { public static final int MAX_FAILURES = 2; private static final String SHA1_SUFFIX = ".sha1"; //$NON-NLS-1$ private static final int SHA1_LEN = 20; private static final String MD5_SUFFIX = ".md5"; //$NON-NLS-1$ private static final int MD5_LEN = 16; private static int hexDigit(byte c) { int v = 0; if (c >= '0' && c <= '9') v = c - '0'; else if (c >= 'a' && c <= 'f') v = (c - 'a') + 10; else if (c >= 'A' && c <= 'F') v = (c - 'A') + 10; return v; } private static byte[] readHex(String name, InputStream stream, int size) throws CoreException, IOException { byte[] buffer = new byte[size * 2]; int bytesRead; int remain = buffer.length; int totRead = 0; while (remain > 0 && (bytesRead = stream.read(buffer, totRead, remain)) > 0) { totRead += bytesRead; remain -= bytesRead; } if (totRead != buffer.length) throw BuckminsterException.fromMessage(NLS.bind(Messages.unable_to_read_the_0_character_hexadecimal_form_of_the_digest_for_1, Integer .valueOf(size), name)); byte[] result = new byte[size]; for (int idx = 0; idx < size; ++idx) { int cidx = idx << 1; int b = (hexDigit(buffer[cidx]) << 4) | hexDigit(buffer[cidx + 1]); result[idx] = (byte) (b & 0xff); } return result; } private static byte[] readRemoteDigest(StringBuilder urlBld, IConnectContext cctx, String suffix, int nBytes) throws CoreException { int len = urlBld.length(); urlBld.append(suffix); String urlStr = urlBld.toString(); urlBld.setLength(len); InputStream input = null; try { input = DownloadManager.read(new URL(urlStr), cctx); return readHex(urlStr, input, nBytes); } catch (IOException e) { return null; } finally { IOUtils.close(input); } } private static void writeHex(byte[] bytes, OutputStream stream) throws IOException { for (int idx = 0; idx < bytes.length; ++idx) { byte b = bytes[idx]; int x = (b & 0xf0) >> 4; stream.write(x >= 10 ? x + ('a' - 10) : x + '0'); x = b & 0x0f; stream.write(x >= 10 ? x + ('a' - 10) : x + '0'); } } private final IPath localCacheRoot; public LocalCache(IPath localCacheRoot) { this.localCacheRoot = localCacheRoot; } public IPath getRootPath() { return localCacheRoot; } public InputStream openFile(URL repository, IConnectContext cctx, IPath path, IProgressMonitor monitor) throws CoreException, IOException { IProgressMonitor subMonitor = monitor; int failureCounter = 0; for (;;) { File localFile; try { localFile = obtainLocalFile(repository, cctx, path, failureCounter, subMonitor); monitor.subTask(Messages.verifying_digest_with_dots); return new FileInputStream(localFile); } catch (CoreException e) { throw e; } catch (FileNotFoundException e) { throw e; } catch (IOException e) { monitor.subTask(Messages.digest_verification_failed + (failureCounter < MAX_FAILURES ? Messages.trying_again_with_dots : "")); //$NON-NLS-1$ if (++failureCounter == MAX_FAILURES) throw e; // Increase the attempt counter and try again. } } } private synchronized File obtainLocalFile(URL repository, IConnectContext cctx, IPath path, int failureCounter, IProgressMonitor monitor) throws IOException, CoreException { IPath fullPath = localCacheRoot.append(path); File file = fullPath.toFile(); StringBuilder urlBld = new StringBuilder(repository.toExternalForm()); if (urlBld.charAt(urlBld.length() - 1) != '/') urlBld.append('/'); urlBld.append(path.toPortableString()); URL remoteURL = new URL(urlBld.toString()); IPath containingFolder = fullPath.removeLastSegments(1); IPath md5Path = containingFolder.append(path.lastSegment() + MD5_SUFFIX); File md5File = md5Path.toFile(); byte[] remoteSha1 = null; byte[] remoteMd5 = null; if ((failureCounter & 1) == 0) { remoteMd5 = readRemoteDigest(urlBld, cctx, MD5_SUFFIX, MD5_LEN); if (remoteMd5 == null) remoteSha1 = readRemoteDigest(urlBld, cctx, SHA1_SUFFIX, SHA1_LEN); } else { remoteSha1 = readRemoteDigest(urlBld, cctx, SHA1_SUFFIX, SHA1_LEN); if (remoteSha1 == null) remoteMd5 = readRemoteDigest(urlBld, cctx, MD5_SUFFIX, MD5_LEN); } byte[] remoteDigest; File localDigestFile; int digestSize; if (remoteMd5 == null) { remoteDigest = remoteSha1; localDigestFile = containingFolder.append(path.lastSegment() + SHA1_SUFFIX).toFile(); digestSize = SHA1_LEN; } else { remoteDigest = remoteMd5; localDigestFile = md5File; digestSize = MD5_LEN; } if (remoteDigest != null) { // Compare local and with remote digest for equality // InputStream input = null; try { input = new FileInputStream(localDigestFile); byte[] localDigest = readHex(localDigestFile.toString(), input, digestSize); if (Arrays.equals(remoteDigest, localDigest)) { // We should have a local file if we have a local digest but // we better make sure // if (file.exists() && file.length() > 0) return file; } } catch (FileNotFoundException e) { // We don't have a local digest. That's OK. } catch (CoreException e) { // The local digest file is corrupt. Disregard... } finally { IOUtils.close(input); } } MessageDigest md; try { md = MessageDigest.getInstance(remoteSha1 == null ? "MD5" //$NON-NLS-1$ : "SHA1"); //$NON-NLS-1$ md.reset(); } catch (NoSuchAlgorithmException e) { throw BuckminsterException.wrap(e); } OutputStream output = null; try { File outputDir = containingFolder.toFile(); if (!(outputDir.exists() || outputDir.mkdirs())) throw new IOException(NLS.bind(Messages.unable_to_create_directory_0, outputDir)); output = new DigestOutputStream(new FileOutputStream(file), md); DownloadManager.readInto(remoteURL, cctx, output, monitor); } finally { IOUtils.close(output); } byte[] localDigest = md.digest(); boolean matchingDigest; if (remoteDigest == null) { MavenPlugin.getLogger().warning(NLS.bind(Messages.unable_to_find_digest_for_0, remoteURL)); matchingDigest = false; } else matchingDigest = Arrays.equals(remoteDigest, localDigest); if (matchingDigest || remoteDigest == null || failureCounter == MAX_FAILURES - 1) { if (remoteDigest != null && !matchingDigest && failureCounter == MAX_FAILURES - 1) // // The maven repo is not perfect. Sometimes the MD5 and SHA1 are // incorrect // due to replace of the actual jar // MavenPlugin.getLogger().warning( NLS.bind(Messages.digest_for_0_still_doesnt_match_after_1_download_attempts_corrupt_repo, remoteURL, new Integer(MAX_FAILURES))); try { output = new FileOutputStream(localDigestFile); writeHex(localDigest, output); } finally { IOUtils.close(output); } return file; } // These one is corrupt somehow // localDigestFile.delete(); file.delete(); throw new IOException(NLS.bind(Messages.digest_mismatch_after_download_for_0, remoteURL)); } }