diff --git a/distribution/archives/build.gradle b/distribution/archives/build.gradle index 5d1703399aa..ae4e6a431c9 100644 --- a/distribution/archives/build.gradle +++ b/distribution/archives/build.gradle @@ -49,7 +49,9 @@ task createPluginsDir(type: EmptyDirTask) { CopySpec archiveFiles(CopySpec modulesFiles, String distributionType, boolean oss) { return copySpec { into("elasticsearch-${version}") { - with libFiles + into('lib') { + with libFiles + } into('config') { dirMode 0750 fileMode 0660 diff --git a/distribution/build.gradle b/distribution/build.gradle index 5f6f0b1579c..fa62513a540 100644 --- a/distribution/build.gradle +++ b/distribution/build.gradle @@ -227,13 +227,15 @@ configure(subprojects.findAll { ['archives', 'packages'].contains(it.name) }) { * Common files in all distributions * *****************************************************************************/ libFiles = copySpec { - into 'lib' + // delay by using closures, since they have not yet been configured, so no jar task exists yet from { project(':server').jar } from { project(':server').configurations.runtime } from { project(':libs:plugin-classloader').jar } - // delay add tools using closures, since they have not yet been configured, so no jar task exists yet from { project(':distribution:tools:launchers').jar } - from { project(':distribution:tools:plugin-cli').jar } + into('tools/plugin-cli') { + from { project(':distribution:tools:plugin-cli').jar } + from { project(':distribution:tools:plugin-cli').configurations.runtime } + } } modulesFiles = { oss -> diff --git a/distribution/packages/build.gradle b/distribution/packages/build.gradle index b15e5668e3d..04fa6313c0a 100644 --- a/distribution/packages/build.gradle +++ b/distribution/packages/build.gradle @@ -124,13 +124,23 @@ Closure commonPackageConfig(String type, boolean oss) { include 'README.textile' fileMode 0644 } + into('lib') { + with copySpec { + with libFiles + // we need to specify every intermediate directory so we iterate through the parents; duplicate calls with the same part are fine + eachFile { FileCopyDetails fcp -> + String[] segments = fcp.relativePath.segments + for (int i = segments.length - 2; i > 0 && segments[i] != 'lib'; --i) { + directory('/' + segments[0..i].join('/'), 0755) + } + fcp.mode = 0644 + } + } + } into('modules') { with copySpec { with modulesFiles(oss) - // we need to specify every intermediate directory, but modules could have sub directories - // and there might not be any files as direct children of intermediates (eg platform) - // so we must iterate through the parents, but duplicate calls with the same path - // are ok (they don't show up in the built packages) + // we need to specify every intermediate directory so we iterate through the parents; duplicate calls with the same part are fine eachFile { FileCopyDetails fcp -> String[] segments = fcp.relativePath.segments for (int i = segments.length - 2; i > 0 && segments[i] != 'modules'; --i) { @@ -251,8 +261,8 @@ ospackage { signingKeyId = project.hasProperty('signing.keyId') ? project.property('signing.keyId') : 'D88E42B4' signingKeyPassphrase = project.property('signing.password') signingKeyRingFile = project.hasProperty('signing.secretKeyRingFile') ? - project.file(project.property('signing.secretKeyRingFile')) : - new File(new File(System.getProperty('user.home'), '.gnupg'), 'secring.gpg') + project.file(project.property('signing.secretKeyRingFile')) : + new File(new File(System.getProperty('user.home'), '.gnupg'), 'secring.gpg') } requires('coreutils') @@ -263,7 +273,6 @@ ospackage { permissionGroup 'root' into '/usr/share/elasticsearch' - with libFiles with noticeFile } diff --git a/distribution/src/bin/elasticsearch-cli b/distribution/src/bin/elasticsearch-cli index 94f8f763bb1..c49c1a51619 100644 --- a/distribution/src/bin/elasticsearch-cli +++ b/distribution/src/bin/elasticsearch-cli @@ -10,6 +10,12 @@ do source "`dirname "$0"`"/$additional_source done +IFS=';' read -r -a additional_classpath_directories <<< "$ES_ADDITIONAL_CLASSPATH_DIRECTORIES" +for additional_classpath_directory in "${additional_classpath_directories[@]}" +do + ES_CLASSPATH="$ES_CLASSPATH:$ES_HOME/$additional_classpath_directory/*" +done + exec \ "$JAVA" \ $ES_JAVA_OPTS \ diff --git a/distribution/src/bin/elasticsearch-cli.bat b/distribution/src/bin/elasticsearch-cli.bat index efda5f653ef..e85abdee448 100644 --- a/distribution/src/bin/elasticsearch-cli.bat +++ b/distribution/src/bin/elasticsearch-cli.bat @@ -11,6 +11,12 @@ for /f "tokens=1*" %%a in ("%*") do ( set arguments=%%b ) +if defined ES_ADDITIONAL_CLASSPATH_DIRECTORIES ( + for %%a in ("%ES_ADDITIONAL_CLASSPATH_DIRECTORIES:;=","%") do ( + set ES_CLASSPATH=!ES_CLASSPATH!;!ES_HOME!/%%a/* + ) +) + %JAVA% ^ %ES_JAVA_OPTS% ^ -Des.path.home="%ES_HOME%" ^ diff --git a/distribution/src/bin/elasticsearch-plugin b/distribution/src/bin/elasticsearch-plugin index 67b6ea7e13c..adfb4a88ad2 100755 --- a/distribution/src/bin/elasticsearch-plugin +++ b/distribution/src/bin/elasticsearch-plugin @@ -1,5 +1,6 @@ #!/bin/bash -"`dirname "$0"`"/elasticsearch-cli \ +ES_ADDITIONAL_CLASSPATH_DIRECTORIES=lib/tools/plugin-cli \ + "`dirname "$0"`"/elasticsearch-cli \ org.elasticsearch.plugins.PluginCli \ "$@" diff --git a/distribution/src/bin/elasticsearch-plugin.bat b/distribution/src/bin/elasticsearch-plugin.bat index d46ef295d08..c9a8e9748f1 100644 --- a/distribution/src/bin/elasticsearch-plugin.bat +++ b/distribution/src/bin/elasticsearch-plugin.bat @@ -3,6 +3,7 @@ setlocal enabledelayedexpansion setlocal enableextensions +set ES_ADDITIONAL_CLASSPATH_DIRECTORIES=lib/tools/plugin-cli call "%~dp0elasticsearch-cli.bat" ^ org.elasticsearch.plugins.PluginCli ^ %* ^ diff --git a/distribution/tools/plugin-cli/build.gradle b/distribution/tools/plugin-cli/build.gradle index 55ec44da25c..c47786299bc 100644 --- a/distribution/tools/plugin-cli/build.gradle +++ b/distribution/tools/plugin-cli/build.gradle @@ -19,14 +19,22 @@ apply plugin: 'elasticsearch.build' +archivesBaseName = 'elasticsearch-plugin-cli' + dependencies { compileOnly "org.elasticsearch:elasticsearch:${version}" compileOnly "org.elasticsearch:elasticsearch-cli:${version}" + compile "org.bouncycastle:bcpg-jdk15on:1.59" + compile "org.bouncycastle:bcprov-jdk15on:1.59" testCompile "org.elasticsearch.test:framework:${version}" testCompile 'com.google.jimfs:jimfs:1.1' testCompile 'com.google.guava:guava:18.0' } +dependencyLicenses { + mapping from: /bc.*/, to: 'bouncycastle' +} + test { // TODO: find a way to add permissions for the tests in this module systemProperty 'tests.security.manager', 'false' diff --git a/distribution/tools/plugin-cli/licenses/bcpg-jdk15on-1.59.jar.sha1 b/distribution/tools/plugin-cli/licenses/bcpg-jdk15on-1.59.jar.sha1 new file mode 100644 index 00000000000..0c0be50c906 --- /dev/null +++ b/distribution/tools/plugin-cli/licenses/bcpg-jdk15on-1.59.jar.sha1 @@ -0,0 +1 @@ +ee93e5376bb6cf0a15c027b5f5e4393f2738e709 \ No newline at end of file diff --git a/distribution/tools/plugin-cli/licenses/bcprov-jdk15on-1.59.jar.sha1 b/distribution/tools/plugin-cli/licenses/bcprov-jdk15on-1.59.jar.sha1 new file mode 100644 index 00000000000..aa42dbb8f69 --- /dev/null +++ b/distribution/tools/plugin-cli/licenses/bcprov-jdk15on-1.59.jar.sha1 @@ -0,0 +1 @@ +2507204241ab450456bdb8e8c0a8f986e418bd99 \ No newline at end of file diff --git a/distribution/tools/plugin-cli/licenses/bouncycastle-LICENSE.txt b/distribution/tools/plugin-cli/licenses/bouncycastle-LICENSE.txt new file mode 100644 index 00000000000..1bd35a7a35c --- /dev/null +++ b/distribution/tools/plugin-cli/licenses/bouncycastle-LICENSE.txt @@ -0,0 +1,17 @@ +Copyright (c) 2000-2015 The Legion of the Bouncy Castle Inc. (http://www.bouncycastle.org) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/distribution/tools/plugin-cli/licenses/bouncycastle-NOTICE.txt b/distribution/tools/plugin-cli/licenses/bouncycastle-NOTICE.txt new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/distribution/tools/plugin-cli/licenses/bouncycastle-NOTICE.txt @@ -0,0 +1 @@ + diff --git a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/InstallPluginCommand.java b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/InstallPluginCommand.java index 6aa9f43936a..a47ea4f8926 100644 --- a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/InstallPluginCommand.java +++ b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/InstallPluginCommand.java @@ -23,6 +23,16 @@ import joptsimple.OptionSet; import joptsimple.OptionSpec; import org.apache.lucene.search.spell.LevensteinDistance; import org.apache.lucene.util.CollectionUtil; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureList; +import org.bouncycastle.openpgp.PGPUtil; +import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory; +import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator; +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider; import org.elasticsearch.Build; import org.elasticsearch.Version; import org.elasticsearch.bootstrap.JarHell; @@ -43,6 +53,7 @@ import java.io.InputStreamReader; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; import java.net.URLConnection; import java.net.URLDecoder; @@ -59,8 +70,11 @@ import java.nio.file.attribute.PosixFileAttributes; import java.nio.file.attribute.PosixFilePermission; import java.nio.file.attribute.PosixFilePermissions; import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.Security; import java.util.ArrayList; import java.util.Arrays; +import java.util.Base64; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -116,7 +130,6 @@ class InstallPluginCommand extends EnvironmentAwareCommand { /** The plugin zip is not properly structured. */ static final int PLUGIN_MALFORMED = 2; - /** The builtin modules, which are plugins, but cannot be installed or removed. */ static final Set MODULES; static { @@ -241,7 +254,7 @@ class InstallPluginCommand extends EnvironmentAwareCommand { if (OFFICIAL_PLUGINS.contains(pluginId)) { final String url = getElasticUrl(terminal, getStagingHash(), Version.CURRENT, isSnapshot(), pluginId, Platforms.PLATFORM_NAME); terminal.println("-> Downloading " + pluginId + " from elastic"); - return downloadZipAndChecksum(terminal, url, tmpDir, false); + return downloadAndValidate(terminal, url, tmpDir, true); } // now try as maven coordinates, a valid URL would only have a colon and slash @@ -249,7 +262,7 @@ class InstallPluginCommand extends EnvironmentAwareCommand { if (coordinates.length == 3 && pluginId.contains("/") == false && pluginId.startsWith("file:") == false) { String mavenUrl = getMavenUrl(terminal, coordinates, Platforms.PLATFORM_NAME); terminal.println("-> Downloading " + pluginId + " from maven central"); - return downloadZipAndChecksum(terminal, mavenUrl, tmpDir, true); + return downloadAndValidate(terminal, mavenUrl, tmpDir, false); } // fall back to plain old URL @@ -406,16 +419,44 @@ class InstallPluginCommand extends EnvironmentAwareCommand { } } - /** Downloads a zip from the url, as well as a SHA512 (or SHA1) checksum, and checks the checksum. */ - // pkg private for tests - @SuppressForbidden(reason = "We use openStream to download plugins") - private Path downloadZipAndChecksum(Terminal terminal, String urlString, Path tmpDir, boolean allowSha1) throws Exception { + @SuppressForbidden(reason = "URL#openStream") + private InputStream urlOpenStream(final URL url) throws IOException { + return url.openStream(); + } + + /** + * Downloads a ZIP from the URL. This method also validates the downloaded plugin ZIP via the following means: + * + * + * @param terminal a terminal to log messages to + * @param urlString the URL of the plugin ZIP + * @param tmpDir a temporary directory to write downloaded files to + * @param officialPlugin true if the plugin is an official plugin + * @return the path to the downloaded plugin ZIP + * @throws IOException if an I/O exception occurs download or reading files and resources + * @throws PGPException if an exception occurs verifying the downloaded ZIP signature + * @throws UserException if checksum validation fails + */ + private Path downloadAndValidate( + final Terminal terminal, + final String urlString, + final Path tmpDir, + final boolean officialPlugin) throws IOException, PGPException, UserException { Path zip = downloadZip(terminal, urlString, tmpDir); pathsToDeleteOnShutdown.add(zip); String checksumUrlString = urlString + ".sha512"; URL checksumUrl = openUrl(checksumUrlString); String digestAlgo = "SHA-512"; - if (checksumUrl == null && allowSha1) { + if (checksumUrl == null && officialPlugin == false) { // fallback to sha1, until 7.0, but with warning terminal.println("Warning: sha512 not found, falling back to sha1. This behavior is deprecated and will be removed in a " + "future release. Please update the plugin to use a sha512 checksum."); @@ -427,7 +468,7 @@ class InstallPluginCommand extends EnvironmentAwareCommand { throw new UserException(ExitCodes.IO_ERROR, "Plugin checksum missing: " + checksumUrlString); } final String expectedChecksum; - try (InputStream in = checksumUrl.openStream()) { + try (InputStream in = urlOpenStream(checksumUrl)) { /* * The supported format of the SHA-1 files is a single-line file containing the SHA-1. The supported format of the SHA-512 files * is a single-line file containing the SHA-512 and the filename, separated by two spaces. For SHA-1, we verify that the hash @@ -465,23 +506,107 @@ class InstallPluginCommand extends EnvironmentAwareCommand { } } - byte[] zipbytes = Files.readAllBytes(zip); - String gotChecksum = MessageDigests.toHexString(MessageDigest.getInstance(digestAlgo).digest(zipbytes)); - if (expectedChecksum.equals(gotChecksum) == false) { - throw new UserException(ExitCodes.IO_ERROR, - digestAlgo + " mismatch, expected " + expectedChecksum + " but got " + gotChecksum); + try { + final byte[] zipBytes = Files.readAllBytes(zip); + final String actualChecksum = MessageDigests.toHexString(MessageDigest.getInstance(digestAlgo).digest(zipBytes)); + if (expectedChecksum.equals(actualChecksum) == false) { + throw new UserException( + ExitCodes.IO_ERROR, + digestAlgo + " mismatch, expected " + expectedChecksum + " but got " + actualChecksum); + } + } catch (final NoSuchAlgorithmException e) { + // this should never happen as we are using SHA-1 and SHA-512 here + throw new AssertionError(e); + } + + if (officialPlugin) { + verifySignature(zip, urlString); } return zip; } + /** + * Verify the signature of the downloaded plugin ZIP. The signature is obtained from the source of the downloaded plugin by appending + * ".asc" to the URL. It is expected that the plugin is signed with the Elastic signing key with ID D27D666CD88E42B4. + * + * @param zip the path to the downloaded plugin ZIP + * @param urlString the URL source of the downloade plugin ZIP + * @throws IOException if an I/O exception occurs reading from various input streams + * @throws PGPException if the PGP implementation throws an internal exception during verification + */ + void verifySignature(final Path zip, final String urlString) throws IOException, PGPException { + final String ascUrlString = urlString + ".asc"; + final URL ascUrl = openUrl(ascUrlString); + try ( + // fin is a file stream over the downloaded plugin zip whose signature to verify + InputStream fin = pluginZipInputStream(zip); + // sin is a URL stream to the signature corresponding to the downloaded plugin zip + InputStream sin = urlOpenStream(ascUrl); + // pin is a decoded base64 stream over the embedded public key in RFC2045 format + InputStream pin = Base64.getMimeDecoder().wrap(getPublicKey())) { + final JcaPGPObjectFactory factory = new JcaPGPObjectFactory(PGPUtil.getDecoderStream(sin)); + final PGPSignature signature = ((PGPSignatureList) factory.nextObject()).get(0); + + // validate the signature has key ID matching our public key ID + final String keyId = Long.toHexString(signature.getKeyID()).toUpperCase(Locale.ROOT); + if (getPublicKeyId().equals(keyId) == false) { + throw new IllegalStateException("key id [" + keyId + "] does not match expected key id [" + getPublicKeyId() + "]"); + } + + // compute the signature of the downloaded plugin zip + final PGPPublicKeyRingCollection collection = new PGPPublicKeyRingCollection(pin, new JcaKeyFingerprintCalculator()); + final PGPPublicKey key = collection.getPublicKey(signature.getKeyID()); + signature.init(new JcaPGPContentVerifierBuilderProvider().setProvider(new BouncyCastleProvider()), key); + final byte[] buffer = new byte[1024]; + int read; + while ((read = fin.read(buffer)) != -1) { + signature.update(buffer, 0, read); + } + + // finally we verify the signature of the downloaded plugin zip matches the expected signature + if (signature.verify() == false) { + throw new IllegalStateException("signature verification for [" + urlString + "] failed"); + } + } + } + + /** + * An input stream to the raw bytes of the plugin ZIP. + * + * @param zip the path to the downloaded plugin ZIP + * @return an input stream to the raw bytes of the plugin ZIP. + * @throws IOException if an I/O exception occurs preparing the input stream + */ + InputStream pluginZipInputStream(final Path zip) throws IOException { + return Files.newInputStream(zip); + } + + /** + * Return the public key ID of the signing key that is expected to have signed the official plugin. + * + * @return the public key ID + */ + String getPublicKeyId() { + return "D27D666CD88E42B4"; + } + + /** + * An input stream to the public key of the signing key. + * + * @return an input stream to the public key + */ + InputStream getPublicKey() { + return InstallPluginCommand.class.getResourceAsStream("/public_key"); + } + /** * Creates a URL and opens a connection. * * If the URL returns a 404, {@code null} is returned, otherwise the open URL opject is returned. */ // pkg private for tests - URL openUrl(String urlString) throws Exception { + URL openUrl(String urlString) throws IOException { URL checksumUrl = new URL(urlString); HttpURLConnection connection = (HttpURLConnection)checksumUrl.openConnection(); if (connection.getResponseCode() == 404) { @@ -605,11 +730,27 @@ class InstallPluginCommand extends EnvironmentAwareCommand { return info; } + private static final String LIB_TOOLS_PLUGIN_CLI_CLASSPATH_JAR; + + static { + LIB_TOOLS_PLUGIN_CLI_CLASSPATH_JAR = + String.format(Locale.ROOT, ".+%1$slib%1$stools%1$splugin-cli%1$s[^%1$s]+\\.jar", "(/|\\\\)"); + } + /** check a candidate plugin for jar hell before installing it */ void jarHellCheck(PluginInfo candidateInfo, Path candidateDir, Path pluginsDir, Path modulesDir) throws Exception { // create list of current jars in classpath - final Set jars = new HashSet<>(JarHell.parseClassPath()); - + final Set classpath = + JarHell.parseClassPath() + .stream() + .filter(url -> { + try { + return url.toURI().getPath().matches(LIB_TOOLS_PLUGIN_CLI_CLASSPATH_JAR) == false; + } catch (final URISyntaxException e) { + throw new AssertionError(e); + } + }) + .collect(Collectors.toSet()); // read existing bundles. this does some checks on the installation too. Set bundles = new HashSet<>(PluginsService.getPluginBundles(pluginsDir)); @@ -621,7 +762,7 @@ class InstallPluginCommand extends EnvironmentAwareCommand { // TODO: optimize to skip any bundles not connected to the candidate plugin? Map> transitiveUrls = new HashMap<>(); for (PluginsService.Bundle bundle : sortedBundles) { - PluginsService.checkBundleJarHell(bundle, transitiveUrls); + PluginsService.checkBundleJarHell(classpath, bundle, transitiveUrls); } // TODO: no jars should be an error diff --git a/distribution/tools/plugin-cli/src/main/resources/public_key b/distribution/tools/plugin-cli/src/main/resources/public_key new file mode 100644 index 00000000000..552c8e3379d --- /dev/null +++ b/distribution/tools/plugin-cli/src/main/resources/public_key @@ -0,0 +1,24 @@ +mQENBFI3HsoBCADXDtbNJnxbPqB1vDNtCsqhe49vFYsZN9IOZsZXgp7aHjh6CJBDA+bGFOwy +hbd7at35jQjWAw1O3cfYsKAmFy+Ar3LHCMkV3oZspJACTIgCrwnkic/9CUliQe324qvObU2Q +RtP4Fl0zWcfb/S8UYzWXWIFuJqMvE9MaRY1bwUBvzoqavLGZj3SF1SPO+TB5QrHkrQHBsmX+ +Jda6d4Ylt8/t6CvMwgQNlrlzIO9WT+YN6zS+sqHd1YK/aY5qhoLNhp9G/HxhcSVCkLq8SStj +1ZZ1S9juBPoXV1ZWNbxFNGwOh/NYGldD2kmBf3YgCqeLzHahsAEpvAm8TBa7Q9W21C8vABEB +AAG0RUVsYXN0aWNzZWFyY2ggKEVsYXN0aWNzZWFyY2ggU2lnbmluZyBLZXkpIDxkZXZfb3Bz +QGVsYXN0aWNzZWFyY2gub3JnPokBOAQTAQIAIgUCUjceygIbAwYLCQgHAwIGFQgCCQoLBBYC +AwECHgECF4AACgkQ0n1mbNiOQrRzjAgAlTUQ1mgo3nK6BGXbj4XAJvuZDG0HILiUt+pPnz75 +nsf0NWhqR4yGFlmpuctgCmTD+HzYtV9fp9qW/bwVuJCNtKXk3sdzYABY+Yl0Cez/7C2GuGCO +lbn0luCNT9BxJnh4mC9h/cKI3y5jvZ7wavwe41teqG14V+EoFSn3NPKmTxcDTFrV7SmVPxCB +cQze00cJhprKxkuZMPPVqpBS+JfDQtzUQD/LSFfhHj9eD+Xe8d7sw+XvxB2aN4gnTlRzjL1n +TRp0h2/IOGkqYfIG9rWmSLNlxhB2t+c0RsjdGM4/eRlPWylFbVMc5pmDpItrkWSnzBfkmXL3 +vO2X3WvwmSFiQbkBDQRSNx7KAQgA5JUlzcMW5/cuyZR8alSacKqhSbvoSqqbzHKcUQZmlzNM +KGTABFG1yRx9r+wa/fvqP6OTRzRDvVS/cycws8YX7Ddum7x8uI95b9ye1/Xy5noPEm8cD+hp +lnpU+PBQZJ5XJ2I+1l9Nixx47wPGXeClLqcdn0ayd+v+Rwf3/XUJrvccG2YZUiQ4jWZkoxsA +07xx7Bj+Lt8/FKG7sHRFvePFU0ZS6JFx9GJqjSBbHRRkam+4emW3uWgVfZxuwcUCn1ayNgRt +KiFv9jQrg2TIWEvzYx9tywTCxc+FFMWAlbCzi+m4WD+QUWWfDQ009U/WM0ks0KwwEwSk/UDu +ToxGnKU2dQARAQABiQEfBBgBAgAJBQJSNx7KAhsMAAoJENJ9ZmzYjkK0c3MIAIE9hAR20mqJ +WLcsxLtrRs6uNF1VrpB+4n/55QU7oxA1iVBO6IFu4qgsF12JTavnJ5MLaETlggXY+zDef9sy +TPXoQctpzcaNVDmedwo1SiL03uMoblOvWpMR/Y0j6rm7IgrMWUDXDPvoPGjMl2q1iTeyHkMZ +EyUJ8SKsaHh4jV9wp9KmC8C+9CwMukL7vM5w8cgvJoAwsp3Fn59AxWthN3XJYcnMfStkIuWg +R7U2r+a210W6vnUxU4oN0PmMcursYPyeV0NX/KQeUeNMwGTFB6QHS/anRaGQewijkrYYoTNt +fllxIu9XYmiBERQ/qPDlGRlOgVTd9xUfHFkzB52c70E= +=92oX diff --git a/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/InstallPluginCommandTests.java b/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/InstallPluginCommandTests.java index 07fe4f5403a..d9238091d87 100644 --- a/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/InstallPluginCommandTests.java +++ b/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/InstallPluginCommandTests.java @@ -23,6 +23,25 @@ import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; import com.google.common.jimfs.Configuration; import com.google.common.jimfs.Jimfs; import org.apache.lucene.util.LuceneTestCase; +import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.bouncycastle.bcpg.BCPGOutputStream; +import org.bouncycastle.bcpg.HashAlgorithmTags; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openpgp.PGPEncryptedData; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPKeyPair; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureGenerator; +import org.bouncycastle.openpgp.operator.PGPDigestCalculator; +import org.bouncycastle.openpgp.operator.bc.BcPBESecretKeyDecryptorBuilder; +import org.bouncycastle.openpgp.operator.bc.BcPGPContentSignerBuilder; +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder; +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder; +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyPair; +import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyEncryptorBuilder; import org.elasticsearch.Build; import org.elasticsearch.Version; import org.elasticsearch.cli.ExitCodes; @@ -44,6 +63,8 @@ import org.junit.After; import org.junit.Before; import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.StringReader; @@ -66,13 +87,19 @@ import java.nio.file.attribute.PosixFileAttributeView; import java.nio.file.attribute.PosixFileAttributes; import java.nio.file.attribute.PosixFilePermission; import java.nio.file.attribute.UserPrincipal; +import java.security.KeyPair; +import java.security.KeyPairGenerator; import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Set; +import java.util.function.BiFunction; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -800,8 +827,16 @@ public class InstallPluginCommandTests extends ESTestCase { skipJarHellCommand.execute(terminal, pluginZip, isBatch, env.v2()); } - void assertInstallPluginFromUrl(String pluginId, String name, String url, String stagingHash, boolean isSnapshot, - String shaExtension, Function shaCalculator) throws Exception { + void assertInstallPluginFromUrl( + final String pluginId, + final String name, + final String url, + final String stagingHash, + final boolean isSnapshot, + final String shaExtension, + final Function shaCalculator, + final PGPSecretKey secretKey, + final BiFunction signature) throws Exception { Tuple env = createEnv(fs, temp); Path pluginDir = createPluginDir(temp); Path pluginZip = createPlugin(name, pluginDir); @@ -814,18 +849,61 @@ public class InstallPluginCommandTests extends ESTestCase { return downloadedPath; } @Override - URL openUrl(String urlString) throws Exception { - String expectedUrl = url + shaExtension; - if (expectedUrl.equals(urlString)) { + URL openUrl(String urlString) throws IOException { + if ((url + shaExtension).equals(urlString)) { // calc sha an return file URL to it Path shaFile = temp.apply("shas").resolve("downloaded.zip" + shaExtension); byte[] zipbytes = Files.readAllBytes(pluginZip); String checksum = shaCalculator.apply(zipbytes); Files.write(shaFile, checksum.getBytes(StandardCharsets.UTF_8)); return shaFile.toUri().toURL(); + } else if ((url + ".asc").equals(urlString)) { + final Path ascFile = temp.apply("asc").resolve("downloaded.zip" + ".asc"); + final byte[] zipBytes = Files.readAllBytes(pluginZip); + final String asc = signature.apply(zipBytes, secretKey); + Files.write(ascFile, asc.getBytes(StandardCharsets.UTF_8)); + return ascFile.toUri().toURL(); } return null; } + + @Override + void verifySignature(Path zip, String urlString) throws IOException, PGPException { + if (InstallPluginCommand.OFFICIAL_PLUGINS.contains(name)) { + super.verifySignature(zip, urlString); + } else { + throw new UnsupportedOperationException("verify signature should not be called for unofficial plugins"); + } + } + + @Override + InputStream pluginZipInputStream(Path zip) throws IOException { + return new ByteArrayInputStream(Files.readAllBytes(zip)); + } + + @Override + String getPublicKeyId() { + return Long.toHexString(secretKey.getKeyID()).toUpperCase(Locale.ROOT); + } + + @Override + InputStream getPublicKey() { + try { + final ByteArrayOutputStream output = new ByteArrayOutputStream(); + final ArmoredOutputStream armored = new ArmoredOutputStream(output); + secretKey.getPublicKey().encode(armored); + armored.close(); + final String publicKey = new String(output.toByteArray(), "UTF-8"); + int start = publicKey.indexOf("\n", 1 + publicKey.indexOf("\n")); + int end = publicKey.lastIndexOf("\n", publicKey.lastIndexOf("\n") - 1); + // strip the header (first two lines) and footer (last line) + final String substring = publicKey.substring(1 + start, end); + return new ByteArrayInputStream(substring.getBytes("UTF-8")); + } catch (final IOException e) { + throw new AssertionError(e); + } + } + @Override boolean urlExists(Terminal terminal, String urlString) throws IOException { return urlString.equals(url); @@ -851,11 +929,12 @@ public class InstallPluginCommandTests extends ESTestCase { public void assertInstallPluginFromUrl( final String pluginId, final String name, final String url, final String stagingHash, boolean isSnapshot) throws Exception { - MessageDigest digest = MessageDigest.getInstance("SHA-512"); - assertInstallPluginFromUrl(pluginId, name, url, stagingHash, isSnapshot, ".sha512", checksumAndFilename(digest, url)); + final MessageDigest digest = MessageDigest.getInstance("SHA-512"); + assertInstallPluginFromUrl( + pluginId, name, url, stagingHash, isSnapshot, ".sha512", checksumAndFilename(digest, url), newSecretKey(), this::signature); } - public void testOfficalPlugin() throws Exception { + public void testOfficialPlugin() throws Exception { String url = "https://artifacts.elastic.co/downloads/elasticsearch-plugins/analysis-icu/analysis-icu-" + Version.CURRENT + ".zip"; assertInstallPluginFromUrl("analysis-icu", "analysis-icu", url, null, false); } @@ -883,13 +962,13 @@ public class InstallPluginCommandTests extends ESTestCase { e, hasToString(containsString("attempted to install release build of official plugin on snapshot build of Elasticsearch"))); } - public void testOfficalPluginStaging() throws Exception { + public void testOfficialPluginStaging() throws Exception { String url = "https://staging.elastic.co/" + Version.CURRENT + "-abc123/downloads/elasticsearch-plugins/analysis-icu/analysis-icu-" + Version.CURRENT + ".zip"; assertInstallPluginFromUrl("analysis-icu", "analysis-icu", url, "abc123", false); } - public void testOfficalPlatformPlugin() throws Exception { + public void testOfficialPlatformPlugin() throws Exception { String url = "https://artifacts.elastic.co/downloads/elasticsearch-plugins/analysis-icu/analysis-icu-" + Platforms.PLATFORM_NAME + "-" + Version.CURRENT + ".zip"; assertInstallPluginFromUrl("analysis-icu", "analysis-icu", url, null, false); @@ -905,7 +984,7 @@ public class InstallPluginCommandTests extends ESTestCase { assertInstallPluginFromUrl("analysis-icu", "analysis-icu", url, "abc123", true); } - public void testOfficalPlatformPluginStaging() throws Exception { + public void testOfficialPlatformPluginStaging() throws Exception { String url = "https://staging.elastic.co/" + Version.CURRENT + "-abc123/downloads/elasticsearch-plugins/analysis-icu/analysis-icu-" + Platforms.PLATFORM_NAME + "-"+ Version.CURRENT + ".zip"; assertInstallPluginFromUrl("analysis-icu", "analysis-icu", url, "abc123", false); @@ -924,7 +1003,7 @@ public class InstallPluginCommandTests extends ESTestCase { public void testMavenSha1Backcompat() throws Exception { String url = "https://repo1.maven.org/maven2/mygroup/myplugin/1.0.0/myplugin-1.0.0.zip"; MessageDigest digest = MessageDigest.getInstance("SHA-1"); - assertInstallPluginFromUrl("mygroup:myplugin:1.0.0", "myplugin", url, null, false, ".sha1", checksum(digest)); + assertInstallPluginFromUrl("mygroup:myplugin:1.0.0", "myplugin", url, null, false, ".sha1", checksum(digest), null, (b, p) -> null); assertTrue(terminal.getOutput(), terminal.getOutput().contains("sha512 not found, falling back to sha1")); } @@ -932,7 +1011,7 @@ public class InstallPluginCommandTests extends ESTestCase { String url = "https://artifacts.elastic.co/downloads/elasticsearch-plugins/analysis-icu/analysis-icu-" + Version.CURRENT + ".zip"; MessageDigest digest = MessageDigest.getInstance("SHA-1"); UserException e = expectThrows(UserException.class, () -> - assertInstallPluginFromUrl("analysis-icu", "analysis-icu", url, null, false, ".sha1", checksum(digest))); + assertInstallPluginFromUrl("analysis-icu", "analysis-icu", url, null, false, ".sha1", checksum(digest), null, (b, p) -> null)); assertEquals(ExitCodes.IO_ERROR, e.exitCode); assertEquals("Plugin checksum missing: " + url + ".sha512", e.getMessage()); } @@ -940,7 +1019,8 @@ public class InstallPluginCommandTests extends ESTestCase { public void testMavenShaMissing() throws Exception { String url = "https://repo1.maven.org/maven2/mygroup/myplugin/1.0.0/myplugin-1.0.0.zip"; UserException e = expectThrows(UserException.class, () -> - assertInstallPluginFromUrl("mygroup:myplugin:1.0.0", "myplugin", url, null, false, ".dne", bytes -> null)); + assertInstallPluginFromUrl( + "mygroup:myplugin:1.0.0", "myplugin", url, null, false, ".dne", bytes -> null, null, (b, p) -> null)); assertEquals(ExitCodes.IO_ERROR, e.exitCode); assertEquals("Plugin checksum missing: " + url + ".sha1", e.getMessage()); } @@ -948,8 +1028,9 @@ public class InstallPluginCommandTests extends ESTestCase { public void testInvalidShaFileMissingFilename() throws Exception { String url = "https://artifacts.elastic.co/downloads/elasticsearch-plugins/analysis-icu/analysis-icu-" + Version.CURRENT + ".zip"; MessageDigest digest = MessageDigest.getInstance("SHA-512"); - UserException e = expectThrows(UserException.class, () -> - assertInstallPluginFromUrl("analysis-icu", "analysis-icu", url, null, false, ".sha512", checksum(digest))); + UserException e = expectThrows(UserException.class, + () -> assertInstallPluginFromUrl( + "analysis-icu", "analysis-icu", url, null, false, ".sha512", checksum(digest), null, (b, p) -> null)); assertEquals(ExitCodes.IO_ERROR, e.exitCode); assertTrue(e.getMessage(), e.getMessage().startsWith("Invalid checksum file")); } @@ -965,7 +1046,9 @@ public class InstallPluginCommandTests extends ESTestCase { null, false, ".sha512", - checksumAndString(digest, " repository-s3-" + Version.CURRENT + ".zip"))); + checksumAndString(digest, " repository-s3-" + Version.CURRENT + ".zip"), + null, + (b, p) -> null)); assertEquals(ExitCodes.IO_ERROR, e.exitCode); assertThat(e, hasToString(matches("checksum file at \\[.*\\] is not for this plugin"))); } @@ -981,7 +1064,9 @@ public class InstallPluginCommandTests extends ESTestCase { null, false, ".sha512", - checksumAndString(digest, " analysis-icu-" + Version.CURRENT + ".zip\nfoobar"))); + checksumAndString(digest, " analysis-icu-" + Version.CURRENT + ".zip\nfoobar"), + null, + (b, p) -> null)); assertEquals(ExitCodes.IO_ERROR, e.exitCode); assertTrue(e.getMessage(), e.getMessage().startsWith("Invalid checksum file")); } @@ -996,7 +1081,9 @@ public class InstallPluginCommandTests extends ESTestCase { null, false, ".sha512", - bytes -> "foobar analysis-icu-" + Version.CURRENT + ".zip")); + bytes -> "foobar analysis-icu-" + Version.CURRENT + ".zip", + null, + (b, p) -> null)); assertEquals(ExitCodes.IO_ERROR, e.exitCode); assertTrue(e.getMessage(), e.getMessage().contains("SHA-512 mismatch, expected foobar")); } @@ -1004,11 +1091,77 @@ public class InstallPluginCommandTests extends ESTestCase { public void testSha1Mismatch() throws Exception { String url = "https://repo1.maven.org/maven2/mygroup/myplugin/1.0.0/myplugin-1.0.0.zip"; UserException e = expectThrows(UserException.class, () -> - assertInstallPluginFromUrl("mygroup:myplugin:1.0.0", "myplugin", url, null, false, ".sha1", bytes -> "foobar")); + assertInstallPluginFromUrl( + "mygroup:myplugin:1.0.0", "myplugin", url, null, false, ".sha1", bytes -> "foobar", null, (b, p) -> null)); assertEquals(ExitCodes.IO_ERROR, e.exitCode); assertTrue(e.getMessage(), e.getMessage().contains("SHA-1 mismatch, expected foobar")); } + public void testPublicKeyIdMismatchToExpectedPublicKeyId() throws Exception { + final String icu = "analysis-icu"; + final String url = + "https://artifacts.elastic.co/downloads/elasticsearch-plugins/analysis-icu/" + icu + "-" + Version.CURRENT + ".zip"; + final MessageDigest digest = MessageDigest.getInstance("SHA-512"); + /* + * To setup a situation where the expected public key ID does not match the public key ID used for signing, we generate a new public + * key at the moment of signing (see the signature invocation). Note that this key will not match the key that we push down to the + * install plugin command. + */ + final PGPSecretKey signingKey = newSecretKey(); // the actual key used for signing + final String actualID = Long.toHexString(signingKey.getKeyID()).toUpperCase(Locale.ROOT); + final BiFunction signature = (b, p) -> signature(b, signingKey); + final PGPSecretKey verifyingKey = newSecretKey(); // the expected key used for signing + final String expectedID = Long.toHexString(verifyingKey.getKeyID()).toUpperCase(Locale.ROOT); + final IllegalStateException e = expectThrows( + IllegalStateException.class, + () -> + assertInstallPluginFromUrl( + icu, icu, url, null, false, ".sha512", checksumAndFilename(digest, url), verifyingKey, signature)); + assertThat(e, hasToString(containsString("key id [" + actualID + "] does not match expected key id [" + expectedID + "]"))); + } + + public void testFailedSignatureVerification() throws Exception { + final String icu = "analysis-icu"; + final String url = + "https://artifacts.elastic.co/downloads/elasticsearch-plugins/analysis-icu/" + icu + "-" + Version.CURRENT + ".zip"; + final MessageDigest digest = MessageDigest.getInstance("SHA-512"); + /* + * To setup a situation where signature verification fails, we will mutate the input byte array by modifying a single byte to some + * random byte value other than the actual value. This is enough to change the signature and cause verification to intentionally + * fail. + */ + final BiFunction signature = (b, p) -> { + final byte[] bytes = Arrays.copyOf(b, b.length); + bytes[0] = randomValueOtherThan(b[0], ESTestCase::randomByte); + return signature(bytes, p); + }; + final IllegalStateException e = expectThrows( + IllegalStateException.class, + () -> + assertInstallPluginFromUrl( + icu, icu, url, null, false, ".sha512", checksumAndFilename(digest, url), newSecretKey(), signature)); + assertThat(e, hasToString(equalTo("java.lang.IllegalStateException: signature verification for [" + url + "] failed"))); + } + + public PGPSecretKey newSecretKey() throws NoSuchAlgorithmException, NoSuchProviderException, PGPException { + final KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(2048); + final KeyPair pair = kpg.generateKeyPair(); + final PGPDigestCalculator sha1Calc = new JcaPGPDigestCalculatorProviderBuilder().build().get(HashAlgorithmTags.SHA1); + final PGPKeyPair pkp = new JcaPGPKeyPair(PGPPublicKey.RSA_GENERAL, pair, new Date()); + return new PGPSecretKey( + PGPSignature.DEFAULT_CERTIFICATION, + pkp, + "example@example.com", + sha1Calc, + null, + null, + new JcaPGPContentSignerBuilder(pkp.getPublicKey().getAlgorithm(), HashAlgorithmTags.SHA1), + new JcePBESecretKeyEncryptorBuilder(PGPEncryptedData.CAST5, sha1Calc) + .setProvider(new BouncyCastleProvider()) + .build("passphrase".toCharArray())); + } + private Function checksum(final MessageDigest digest) { return checksumAndString(digest, ""); } @@ -1022,6 +1175,32 @@ public class InstallPluginCommandTests extends ESTestCase { return bytes -> MessageDigests.toHexString(digest.digest(bytes)) + s; } + private String signature(final byte[] bytes, final PGPSecretKey secretKey) { + try { + final PGPPrivateKey privateKey + = secretKey.extractPrivateKey( + new BcPBESecretKeyDecryptorBuilder( + new JcaPGPDigestCalculatorProviderBuilder().build()).build("passphrase".toCharArray())); + final PGPSignatureGenerator generator = + new PGPSignatureGenerator( + new BcPGPContentSignerBuilder(privateKey.getPublicKeyPacket().getAlgorithm(), HashAlgorithmTags.SHA512)); + generator.init(PGPSignature.BINARY_DOCUMENT, privateKey); + final ByteArrayOutputStream output = new ByteArrayOutputStream(); + try (BCPGOutputStream pout = new BCPGOutputStream(new ArmoredOutputStream(output)); + InputStream is = new ByteArrayInputStream(bytes)) { + final byte[] buffer = new byte[1024]; + int read; + while ((read = is.read(buffer)) != -1) { + generator.update(buffer, 0, read); + } + generator.generate().encode(pout); + } + return new String(output.toByteArray(), "UTF-8"); + } catch (IOException | PGPException e) { + throw new RuntimeException(e); + } + } + // checks the plugin requires a policy confirmation, and does not install when that is rejected by the user // the plugin is installed after this method completes private void assertPolicyConfirmation(Tuple env, String pluginZip, String... warnings) throws Exception { diff --git a/server/src/main/java/org/elasticsearch/plugins/PluginsService.java b/server/src/main/java/org/elasticsearch/plugins/PluginsService.java index 3bb2c3a1868..68a19bb9bca 100644 --- a/server/src/main/java/org/elasticsearch/plugins/PluginsService.java +++ b/server/src/main/java/org/elasticsearch/plugins/PluginsService.java @@ -440,7 +440,7 @@ public class PluginsService extends AbstractComponent { List sortedBundles = sortBundles(bundles); for (Bundle bundle : sortedBundles) { - checkBundleJarHell(bundle, transitiveUrls); + checkBundleJarHell(JarHell.parseClassPath(), bundle, transitiveUrls); final Plugin plugin = loadBundle(bundle, loaded); plugins.add(new Tuple<>(bundle.plugin, plugin)); @@ -451,7 +451,7 @@ public class PluginsService extends AbstractComponent { // jar-hell check the bundle against the parent classloader and extended plugins // the plugin cli does it, but we do it again, in case lusers mess with jar files manually - static void checkBundleJarHell(Bundle bundle, Map> transitiveUrls) { + static void checkBundleJarHell(Set classpath, Bundle bundle, Map> transitiveUrls) { // invariant: any plugins this plugin bundle extends have already been added to transitiveUrls List exts = bundle.plugin.getExtendedPlugins(); @@ -484,7 +484,6 @@ public class PluginsService extends AbstractComponent { JarHell.checkJarHell(urls, logger::debug); // check jarhell of each extended plugin against this plugin transitiveUrls.put(bundle.plugin.getName(), urls); - Set classpath = JarHell.parseClassPath(); // check we don't have conflicting codebases with core Set intersection = new HashSet<>(classpath); intersection.retainAll(bundle.urls); diff --git a/server/src/test/java/org/elasticsearch/plugins/PluginsServiceTests.java b/server/src/test/java/org/elasticsearch/plugins/PluginsServiceTests.java index ffecaca4525..5f1d1f612d7 100644 --- a/server/src/test/java/org/elasticsearch/plugins/PluginsServiceTests.java +++ b/server/src/test/java/org/elasticsearch/plugins/PluginsServiceTests.java @@ -23,6 +23,7 @@ import org.apache.log4j.Level; import org.apache.lucene.util.Constants; import org.apache.lucene.util.LuceneTestCase; import org.elasticsearch.Version; +import org.elasticsearch.bootstrap.JarHell; import org.elasticsearch.common.io.PathUtils; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.env.Environment; @@ -443,7 +444,7 @@ public class PluginsServiceTests extends ESTestCase { "MyPlugin", Collections.singletonList("dep"), false); PluginsService.Bundle bundle = new PluginsService.Bundle(info1, pluginDir); IllegalStateException e = expectThrows(IllegalStateException.class, () -> - PluginsService.checkBundleJarHell(bundle, transitiveDeps)); + PluginsService.checkBundleJarHell(JarHell.parseClassPath(), bundle, transitiveDeps)); assertEquals("failed to load plugin myplugin due to jar hell", e.getMessage()); assertThat(e.getCause().getMessage(), containsString("jar hell! duplicate codebases with extended plugin")); } @@ -462,7 +463,7 @@ public class PluginsServiceTests extends ESTestCase { "MyPlugin", Arrays.asList("dep1", "dep2"), false); PluginsService.Bundle bundle = new PluginsService.Bundle(info1, pluginDir); IllegalStateException e = expectThrows(IllegalStateException.class, () -> - PluginsService.checkBundleJarHell(bundle, transitiveDeps)); + PluginsService.checkBundleJarHell(JarHell.parseClassPath(), bundle, transitiveDeps)); assertEquals("failed to load plugin myplugin due to jar hell", e.getMessage()); assertThat(e.getCause().getMessage(), containsString("jar hell!")); assertThat(e.getCause().getMessage(), containsString("duplicate codebases")); @@ -479,7 +480,7 @@ public class PluginsServiceTests extends ESTestCase { "MyPlugin", Collections.emptyList(), false); PluginsService.Bundle bundle = new PluginsService.Bundle(info1, pluginDir); IllegalStateException e = expectThrows(IllegalStateException.class, () -> - PluginsService.checkBundleJarHell(bundle, new HashMap<>())); + PluginsService.checkBundleJarHell(JarHell.parseClassPath(), bundle, new HashMap<>())); assertEquals("failed to load plugin myplugin due to jar hell", e.getMessage()); assertThat(e.getCause().getMessage(), containsString("jar hell!")); assertThat(e.getCause().getMessage(), containsString("Level")); @@ -498,7 +499,7 @@ public class PluginsServiceTests extends ESTestCase { "MyPlugin", Collections.singletonList("dep"), false); PluginsService.Bundle bundle = new PluginsService.Bundle(info1, pluginDir); IllegalStateException e = expectThrows(IllegalStateException.class, () -> - PluginsService.checkBundleJarHell(bundle, transitiveDeps)); + PluginsService.checkBundleJarHell(JarHell.parseClassPath(), bundle, transitiveDeps)); assertEquals("failed to load plugin myplugin due to jar hell", e.getMessage()); assertThat(e.getCause().getMessage(), containsString("jar hell!")); assertThat(e.getCause().getMessage(), containsString("DummyClass1")); @@ -521,7 +522,7 @@ public class PluginsServiceTests extends ESTestCase { "MyPlugin", Arrays.asList("dep1", "dep2"), false); PluginsService.Bundle bundle = new PluginsService.Bundle(info1, pluginDir); IllegalStateException e = expectThrows(IllegalStateException.class, () -> - PluginsService.checkBundleJarHell(bundle, transitiveDeps)); + PluginsService.checkBundleJarHell(JarHell.parseClassPath(), bundle, transitiveDeps)); assertEquals("failed to load plugin myplugin due to jar hell", e.getMessage()); assertThat(e.getCause().getMessage(), containsString("jar hell!")); assertThat(e.getCause().getMessage(), containsString("DummyClass2")); @@ -543,7 +544,7 @@ public class PluginsServiceTests extends ESTestCase { PluginInfo info1 = new PluginInfo("myplugin", "desc", "1.0", Version.CURRENT, "1.8", "MyPlugin", Arrays.asList("dep1", "dep2"), false); PluginsService.Bundle bundle = new PluginsService.Bundle(info1, pluginDir); - PluginsService.checkBundleJarHell(bundle, transitiveDeps); + PluginsService.checkBundleJarHell(JarHell.parseClassPath(), bundle, transitiveDeps); Set deps = transitiveDeps.get("myplugin"); assertNotNull(deps); assertThat(deps, containsInAnyOrder(pluginJar.toUri().toURL(), dep1Jar.toUri().toURL(), dep2Jar.toUri().toURL()));