From ca241b4bf65d64527f2165935420d31bbd252a2d Mon Sep 17 00:00:00 2001 From: Basil Crow Date: Mon, 29 Jul 2024 17:57:31 -0700 Subject: [PATCH] Use detached plugins as a cache for the Update Center (#9476) --- .../main/java/hudson/model/UpdateCenter.java | 157 +++++++++++++++++- 1 file changed, 156 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/hudson/model/UpdateCenter.java b/core/src/main/java/hudson/model/UpdateCenter.java index 218cbf6b96..4df95b7b3f 100644 --- a/core/src/main/java/hudson/model/UpdateCenter.java +++ b/core/src/main/java/hudson/model/UpdateCenter.java @@ -58,14 +58,18 @@ import hudson.util.NamingThreadFactory; import hudson.util.PersistedList; import hudson.util.VersionNumber; import hudson.util.XStream2; +import java.io.BufferedInputStream; import java.io.File; +import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.io.UncheckedIOException; import java.lang.reflect.Constructor; import java.net.HttpRetryException; import java.net.HttpURLConnection; import java.net.MalformedURLException; +import java.net.URISyntaxException; import java.net.URL; import java.net.URLConnection; import java.net.UnknownHostException; @@ -86,6 +90,7 @@ import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.TreeSet; import java.util.UUID; @@ -1809,6 +1814,83 @@ public class UpdateCenter extends AbstractModelObject implements Loadable, Savea String getComputedSHA512(); } + @SuppressFBWarnings(value = "WEAK_MESSAGE_DIGEST_SHA1", justification = "SHA-1 is only used as a fallback if SHA-256/SHA-512 are not available") + private static class FileWithComputedChecksums implements WithComputedChecksums { + + private final File file; + + private String computedSHA1; + private String computedSHA256; + private String computedSHA512; + + FileWithComputedChecksums(File file) { + this.file = Objects.requireNonNull(file); + } + + @Override + public synchronized String getComputedSHA1() { + if (computedSHA1 != null) { + return computedSHA1; + } + + MessageDigest messageDigest; + try { + messageDigest = MessageDigest.getInstance("SHA-1"); + } catch (NoSuchAlgorithmException e) { + throw new UnsupportedOperationException(e); + } + computedSHA1 = computeDigest(messageDigest); + return computedSHA1; + } + + @Override + public synchronized String getComputedSHA256() { + if (computedSHA256 != null) { + return computedSHA256; + } + + MessageDigest messageDigest; + try { + messageDigest = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new UnsupportedOperationException(e); + } + computedSHA256 = computeDigest(messageDigest); + return computedSHA256; + } + + @Override + public synchronized String getComputedSHA512() { + if (computedSHA512 != null) { + return computedSHA512; + } + + MessageDigest messageDigest; + try { + messageDigest = MessageDigest.getInstance("SHA-512"); + } catch (NoSuchAlgorithmException e) { + throw new UnsupportedOperationException(e); + } + computedSHA512 = computeDigest(messageDigest); + return computedSHA512; + } + + private String computeDigest(MessageDigest digest) { + try (InputStream is = new FileInputStream(file); + BufferedInputStream bis = new BufferedInputStream(is)) { + byte[] buffer = new byte[1024]; + int read = bis.read(buffer, 0, buffer.length); + while (read > -1) { + digest.update(buffer, 0, read); + read = bis.read(buffer, 0, buffer.length); + } + return Base64.getEncoder().encodeToString(digest.digest()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + } + /** * Base class for a job that downloads a file from the Jenkins project. */ @@ -2245,7 +2327,24 @@ public class UpdateCenter extends AbstractModelObject implements Loadable, Savea return; } try { - super._run(); + File cached = getCached(this); + if (cached != null) { + File dst = getDestination(); + + // A bit naive, but following the corresponding logic in UpdateCenterConfiguration#download... + File tmp = new File(dst.getPath() + ".tmp"); + Files.copy(cached.toPath(), tmp.toPath(), StandardCopyOption.REPLACE_EXISTING); + + config.postValidate(this, tmp); + + /* + * Will unfortunately validate the checksum a second time, but this should still be faster than + * network I/O and at least allows us to reuse code... + */ + config.install(this, tmp, dst); + } else { + super._run(); + } // if this is a bundled plugin, make sure it won't get overwritten PluginWrapper pw = plugin.getInstalled(); @@ -2278,6 +2377,62 @@ public class UpdateCenter extends AbstractModelObject implements Loadable, Savea } } + /** + * If we happen to have the file already in the {@coode WEB-INF/detached-plugins} directory and it happens to + * match the checksum we were expecting, then save ourselves a trip to the download site. This method is + * best-effort, and if anything goes wrong we simply fall back to the standard download path. + * + * @return The cached file, or null for a cache miss + */ + @CheckForNull + private File getCached(DownloadJob job) { + URL src; + try { + /* + * Could make PluginManager#getDetachedLocation public and consume it here, but this method is + * best-effort anyway. + */ + src = Jenkins.get().servletContext.getResource(String.format("/WEB-INF/detached-plugins/%s.hpi", plugin.name)); + } catch (MalformedURLException e) { + return null; + } + + if (src == null || !"file".equals(src.getProtocol())) { + return null; + } + + try { + config.preValidate(this, src); + } catch (IOException e) { + return null; + } + + File cached; + try { + cached = new File(src.toURI()); + } catch (URISyntaxException e) { + return null; + } + + if (!cached.isFile()) { + return null; + } + + WithComputedChecksums withComputedChecksums = new FileWithComputedChecksums(cached); + try { + verifyChecksums(withComputedChecksums, plugin, cached); + } catch (IOException | UncheckedIOException | UnsupportedOperationException e) { + return null; + } + + // Allow us to reuse UpdateCenter.InstallationJob#replace. + job.computedSHA1 = withComputedChecksums.getComputedSHA1(); + job.computedSHA256 = withComputedChecksums.getComputedSHA256(); + job.computedSHA512 = withComputedChecksums.getComputedSHA512(); + + return cached; + } + /** * Indicates there is another installation job for this plugin * @since 2.1