GUACAMOLE-2139: Automatically determine extension compatibility.

This commit is contained in:
Michael Jumper committed 2025-12-24 13:17:06 -08:00
1 parent 451f5aad9d
commit 563bd9cb14
5 files changed
+344 -39

No files matched your search

+7
View File
@@ -374,6 +374,13 @@
<version>1.2.2</version> <version>1.2.2</version>
</dependency> </dependency>
<!-- JUnit for unit testing -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
</project> </project>
@@ -0,0 +1,159 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.extension;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Representation of the Guacamole webapp version. This is used both to
* represent the expected minimum webapp version required by an extension and
* to represent the running version of Guacamole.
*/
public class GuacamoleVersion {
/**
* Regular expression that splits a version number into its major, minor,
* and patch components, as well as an optional version suffix.
*/
private static final Pattern VERSION_PATTERN = Pattern.compile("(\\d+)\\.(\\d+)\\.(\\d+)(.+)?");
/**
* The version of this copy of the Guacamole web application. This value is
* substituted by Maven during the build.
*/
public static final GuacamoleVersion RUNNING_VERSION = new GuacamoleVersion("${project.version}");
/**
* The first (leftmost) version number of a Guacamole version. This is the
* number of greatest significance and denotes compatibility. If two
* Guacamole versions have the same major number, extensions for the older
* version should still work with the newer version.
*/
private final int major;
/**
* The second (middle) version number of a Guacamole version. If two
* Guacamole versions have the same major number but different minor
* numbers, they would be expected to differ in the features available.
*/
private final int minor;
/**
* The third (rightmost) version number of a Guacamole version. This is the
* number of least significance and denotes minor changes or fixes. If
* two Guacamole versions have the same major and minor numbers but
* different patch numbers, the newer version would be expected to have
* fixes or improvements that correct issues affecting the older version,
* but would not be expected to have major new features.
*/
private final int patch;
/**
* An arbitrary suffix that might be appended by downstream package
* maintainers or vendors. A Guacamole version with a particular suffix
* would be expected to have patches applied relative to the version without
* the suffix but is otherwise compatible (as if the patch or minor numbers
* differed instead of the suffix).
*/
private final String suffix;
/**
* Creates a new GuacamoleVersion representing the version of Guacamole
* that has the given version string. The version string must be in the
* format "MAJOR.MINOR.PATCH" with an optional and arbitrary version
* suffix.
*
* @param versionStr
* The Guacamole version string to parse.
*
* @throws IllegalArgumentException
* If the version string is not in the correct format.
*/
public GuacamoleVersion(String versionStr) throws IllegalArgumentException {
// Use all-zeroes without suffix to represent the wildcard version
if ("*".equals(versionStr)) {
major = minor = patch = 0;
suffix = null;
}
// Extract major/minor/patch and optional suffix from version string
else {
Matcher versionMatcher = VERSION_PATTERN.matcher(versionStr);
if (!versionMatcher.matches())
throw new IllegalArgumentException("Format of version "
+ "string \"" + versionStr + "\" is not valid");
major = Integer.parseInt(versionMatcher.group(1));
minor = Integer.parseInt(versionMatcher.group(2));
patch = Integer.parseInt(versionMatcher.group(3));
suffix = versionMatcher.group(4);
}
}
/**
* Returns whether this Guacamole version is expected to be compatible with
* an extension that requires at least the given Guacamole version.
*
* @param required
* The minimum Guacamole version required.
*
* @return
* true if this Guacamole version is expected to be compatible with an
* extension requiring at least the given version, false otherwise.
*/
public boolean provides(GuacamoleVersion required) {
// Wildcard version always matches
if (required.major == 0 && required.minor == 0 && required.patch == 0
&& required.suffix == null)
return true;
// Major version component MUST be identical
if (this.major != required.major)
return false;
// Minor version component must be the same or greater than the
// requirement
if (this.minor < required.minor)
return false;
// If both major and minor are identical, the patch version component
// must be the same or greater than the requirement
if (this.minor == required.minor && this.patch < required.patch)
return false;
// Any suffix denotes vendor-specific patching that must be assumed to
// be an arbitrary requirement only satisfied by that suffix. If no
// suffix is required, any suffix of the current Guacamole version can
// just be ignored.
if (required.suffix == null)
return true;
return required.suffix.equals(this.suffix);
}
}
@@ -24,7 +24,6 @@ import com.google.inject.servlet.ServletModule;
import java.io.File; import java.io.File;
import java.io.FileFilter; import java.io.FileFilter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
@@ -57,27 +56,6 @@ public class ExtensionModule extends ServletModule {
*/ */
private final Logger logger = LoggerFactory.getLogger(ExtensionModule.class); private final Logger logger = LoggerFactory.getLogger(ExtensionModule.class);
/**
* The version strings of all Guacamole versions whose extensions are
* compatible with this release.
*/
private static final List<String> ALLOWED_GUACAMOLE_VERSIONS =
Collections.unmodifiableList(Arrays.asList(
"*",
"1.0.0",
"1.1.0",
"1.2.0",
"1.3.0",
"1.4.0",
"1.5.0",
"1.5.1",
"1.5.2",
"1.5.3",
"1.5.4",
"1.5.5",
"1.6.0"
));
/** /**
* The name of the directory within GUACAMOLE_HOME containing any .jars * The name of the directory within GUACAMOLE_HOME containing any .jars
* which should be included in the classpath of all extensions. * which should be included in the classpath of all extensions.
@@ -394,21 +372,6 @@ public class ExtensionModule extends ServletModule {
} }
/**
* Returns whether the given version of Guacamole is compatible with this
* version of Guacamole as far as extensions are concerned.
*
* @param guacamoleVersion
* The version of Guacamole the extension was built for.
*
* @return
* true if the given version of Guacamole is compatible with this
* version of Guacamole, false otherwise.
*/
private boolean isCompatible(String guacamoleVersion) {
return ALLOWED_GUACAMOLE_VERSIONS.contains(guacamoleVersion);
}
/** /**
* Returns the set of identifiers of all authentication providers whose * Returns the set of identifiers of all authentication providers whose
* internal failures should be tolerated during the authentication process. * internal failures should be tolerated during the authentication process.
@@ -513,7 +476,8 @@ public class ExtensionModule extends ServletModule {
Extension extension = new Extension(getParentClassLoader(), extensionFile, temporaryFiles); Extension extension = new Extension(getParentClassLoader(), extensionFile, temporaryFiles);
// Validate Guacamole version of extension // Validate Guacamole version of extension
if (!isCompatible(extension.getGuacamoleVersion())) { GuacamoleVersion requiredVersion = new GuacamoleVersion(extension.getGuacamoleVersion());
if (!GuacamoleVersion.RUNNING_VERSION.provides(requiredVersion)) {
logger.debug("Declared Guacamole version \"{}\" of extension \"{}\" is not compatible with this version of Guacamole.", logger.debug("Declared Guacamole version \"{}\" of extension \"{}\" is not compatible with this version of Guacamole.",
extension.getGuacamoleVersion(), extensionFile.getName()); extension.getGuacamoleVersion(), extensionFile.getName());
throw new GuacamoleServerException("Extension \"" + extension.getName() + "\" is not " throw new GuacamoleServerException("Extension \"" + extension.getName() + "\" is not "
@@ -523,7 +487,7 @@ public class ExtensionModule extends ServletModule {
extensions.add(extension); extensions.add(extension);
} }
catch (GuacamoleException e) { catch (GuacamoleException | IllegalArgumentException e) {
logger.error("Extension \"{}\" could not be loaded: {}", logger.error("Extension \"{}\" could not be loaded: {}",
extensionFile.getName(), e.getMessage(), e); extensionFile.getName(), e.getMessage(), e);
} }
@@ -0,0 +1,150 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.extension;
import java.util.Arrays;
import org.junit.Assert;
import org.junit.Test;
/**
* Unit tests which verify GuacamoleVersion correctly parses and compares
* Guacamole versions.
*/
public class GuacamoleVersionTest {
/**
* Verifies that trivially valid version numbers are correctly parsed.
*/
@Test
public void testValidVersion() {
GuacamoleVersion version = new GuacamoleVersion("1.2.3");
Assert.assertTrue(version.provides(version));
}
/**
* Verifies that invalid version numbers correctly fail to parse.
*/
@Test
public void testInvalidVersion() {
for (String invalid : Arrays.asList("1.2")) {
try {
GuacamoleVersion version = new GuacamoleVersion(invalid);
Assert.fail("Invalid Guacamole version string \""
+ invalid + "\" should have failed to parse.");
}
catch (IllegalArgumentException e) {
// Expected
}
}
}
/**
* Verifies that newer versions are recognized as providing older versions
* when they differ by minor number (but not the other way around).
*/
@Test
public void testMinorNewerProvidesOlder() {
GuacamoleVersion olderVersion = new GuacamoleVersion("1.2.3");
GuacamoleVersion newerVersion = new GuacamoleVersion("1.22.3");
Assert.assertTrue(newerVersion.provides(olderVersion));
Assert.assertFalse(olderVersion.provides(newerVersion));
}
/**
* Verifies that newer versions are recognized as providing older versions
* when they differ by patch number (but not the other way around).
*/
@Test
public void testPatchNewerProvidesOlder() {
GuacamoleVersion olderVersion = new GuacamoleVersion("1.2.3");
GuacamoleVersion newerVersion = new GuacamoleVersion("1.2.7");
Assert.assertTrue(newerVersion.provides(olderVersion));
Assert.assertFalse(olderVersion.provides(newerVersion));
}
/**
* Verifies that two versions are NOT recognized as providing each other
* when they differ by major number (regardless of which is newer).
*/
@Test
public void testPatchMajorIncompatible() {
GuacamoleVersion olderVersion = new GuacamoleVersion("1.2.3");
GuacamoleVersion newerVersion = new GuacamoleVersion("2.2.3");
Assert.assertFalse(newerVersion.provides(olderVersion));
Assert.assertFalse(olderVersion.provides(newerVersion));
}
/**
* Verifies that a version number with an arbitrary suffix is recognized
* as providing that same version without the suffix (but not the other way
* around).
*/
@Test
public void testCompatibleSuffix() {
GuacamoleVersion olderVersion = new GuacamoleVersion("1.2.3");
GuacamoleVersion newerVersion = new GuacamoleVersion("1.2.3-foo");
Assert.assertTrue(newerVersion.provides(olderVersion));
Assert.assertFalse(olderVersion.provides(newerVersion));
}
/**
* Verifies that two version numbers that differ by arbitrary suffix are
* NOT recognized as providing each other, even when the version numbers
* are otherwise the same.
*/
@Test
public void testIncompatibleSuffix() {
GuacamoleVersion olderVersion = new GuacamoleVersion("1.2.3-bar");
GuacamoleVersion newerVersion = new GuacamoleVersion("1.2.3-foo");
Assert.assertFalse(newerVersion.provides(olderVersion));
Assert.assertFalse(olderVersion.provides(newerVersion));
}
/**
* Verifies that the special wildcard version is provided by any version,
* but not the other way around.
*/
@Test
public void testWildcardVersion() {
GuacamoleVersion specificVersion = new GuacamoleVersion("1.2.3-foo");
GuacamoleVersion wildcardVersion = new GuacamoleVersion("*");
Assert.assertTrue(specificVersion.provides(wildcardVersion));
Assert.assertFalse(wildcardVersion.provides(specificVersion));
}
}
+25
View File
@@ -490,6 +490,31 @@
</build> </build>
</profile> </profile>
<!-- Automatically filter Java source if located within src/main/java-templates -->
<profile>
<id>filter-java-source</id>
<activation>
<file><exists>src/main/java-templates</exists></file>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>templating-maven-plugin</artifactId>
<version>3.0.0</version>
<executions>
<execution>
<id>filter-java-source</id>
<goals>
<goal>filter-sources</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<!-- Automatically filter resources if located within src/main/resource-templates --> <!-- Automatically filter resources if located within src/main/resource-templates -->
<profile> <profile>
<id>filter-resources</id> <id>filter-resources</id>