# HG changeset patch # User Andrew Azores # Date 1406840259 14400 # Node ID 36ecc6fe6f6273ca12dbde601ba15e526796bd68 # Parent 39631aab56cfc56b3ed1aa8bde7541c34b9dcc17 URLPermissions granted in SecurityDesc if available 2014-07-31 Andrew Azores Add URLPermission support to SecurityDesc. This is essentially Java 8 support, as URLPermission is new to Java 8 and required for many applets to continue working when a Java 8-compatible JVM is in use. * netx/net/sourceforge/jnlp/SecurityDesc.java (urlPermissionClass, urlPermissionConstructor): new static variables for storing references to URLPermission, if available, for reflective construction at runtime (getSandboxPermissions): adds URLPermissions to sandbox permissions set, if available (Java 8+) (getUrlPermissions): new method for getting URLPermissions for the current SecurityDesc (getHostWithSpecifiedPort, appendRecursiveSubdirToCodebaseHostString): new static helper methods for generating URLPermissions' constructor args (requireNonNull): new method, simply throws NPE if its argument is null * tests/netx/unit/net/sourceforge/jnlp/SecurityDescTest.java (testNotNullJnlpFile): cleanup refactor, no semantic change (testNullJnlpFile, testAppendRecursiveSubdirToCodebaseHostString, testAppendRecursiveSubdirToCodebaseHostString2, testAppendRecursiveSubdirToCodebaseHostString3, testAppendRecursiveSubdirToCodebaseHostStringWithPort, testAppendRecursiveSubdirToCodebaseHostStringWithNull, testGetHostWithSpecifiedPort, testGetHostWithSpecifiedPortWithFtpScheme, testGetHostWithSpecifiedPortWithUserInfo, testGetHostWithSpecifiedPOrtWithPort, testGetHostWithSpecifiedPortWithPath, testGetHostWithSpecifiedPortWithAll, testGetHostWithSpecifiedPortWithNull, testGetHost, testGetHostWithFtpScheme, testGetHostWithUserInfo, testGetHostWithPort, testGetHostWithPath, testGetHostWithAll, testGetHostNull, testGetHostWithAppendRecursiveSubdirToCodebaseHostString, testGetHostWithSpecifiedPortWithAppendRecursiveSubdirToCodebaseHostString): new test methods diff -r 39631aab56cf -r 36ecc6fe6f62 ChangeLog --- a/ChangeLog Thu Jul 31 09:19:59 2014 -0400 +++ b/ChangeLog Thu Jul 31 16:57:39 2014 -0400 @@ -1,3 +1,36 @@ +2014-07-31 Andrew Azores + + Add URLPermission support to SecurityDesc. This is essentially Java 8 + support, as URLPermission is new to Java 8 and required for many applets + to continue working when a Java 8-compatible JVM is in use. + * netx/net/sourceforge/jnlp/SecurityDesc.java (urlPermissionClass, + urlPermissionConstructor): new static variables for storing references to + URLPermission, if available, for reflective construction at runtime + (getSandboxPermissions): adds URLPermissions to sandbox permissions set, + if available (Java 8+) + (getUrlPermissions): new method for getting URLPermissions for the current + SecurityDesc + (getHostWithSpecifiedPort, appendRecursiveSubdirToCodebaseHostString): new + static helper methods for generating URLPermissions' constructor args + (requireNonNull): new method, simply throws NPE if its argument is null + * tests/netx/unit/net/sourceforge/jnlp/SecurityDescTest.java + (testNotNullJnlpFile): cleanup refactor, no semantic change + (testNullJnlpFile, testAppendRecursiveSubdirToCodebaseHostString, + testAppendRecursiveSubdirToCodebaseHostString2, + testAppendRecursiveSubdirToCodebaseHostString3, + testAppendRecursiveSubdirToCodebaseHostStringWithPort, + testAppendRecursiveSubdirToCodebaseHostStringWithNull, + testGetHostWithSpecifiedPort, testGetHostWithSpecifiedPortWithFtpScheme, + testGetHostWithSpecifiedPortWithUserInfo, + testGetHostWithSpecifiedPOrtWithPort, + testGetHostWithSpecifiedPortWithPath, testGetHostWithSpecifiedPortWithAll, + testGetHostWithSpecifiedPortWithNull, testGetHost, + testGetHostWithFtpScheme, testGetHostWithUserInfo, testGetHostWithPort, + testGetHostWithPath, testGetHostWithAll, testGetHostNull, + testGetHostWithAppendRecursiveSubdirToCodebaseHostString, + testGetHostWithSpecifiedPortWithAppendRecursiveSubdirToCodebaseHostString): + new test methods + 2014-07-31 Andrew Azores Fixes for coverity issues discovered in RH1121549 diff -r 39631aab56cf -r 36ecc6fe6f62 NEWS --- a/NEWS Thu Jul 31 09:19:59 2014 -0400 +++ b/NEWS Thu Jul 31 16:57:39 2014 -0400 @@ -10,6 +10,7 @@ New in release 1.5.1 (YYYY-MM-DD): * Improved to be able to run with any JDK +* JDK 8 support added (URLPermission granted if applicable) * Added DE and PL localizations * Added KEY_ENABLE_MANIFEST_ATTRIBUTES_CHECK deployment property to control scan of Manifest file * Control Panel diff -r 39631aab56cf -r 36ecc6fe6f62 netx/net/sourceforge/jnlp/SecurityDesc.java --- a/netx/net/sourceforge/jnlp/SecurityDesc.java Thu Jul 31 09:19:59 2014 -0400 +++ b/netx/net/sourceforge/jnlp/SecurityDesc.java Thu Jul 31 16:57:39 2014 -0400 @@ -16,11 +16,25 @@ package net.sourceforge.jnlp; -import java.io.*; -import java.net.*; -import java.util.*; -import java.security.*; import java.awt.AWTPermission; +import java.io.FilePermission; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.net.SocketPermission; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.AllPermission; +import java.security.CodeSource; +import java.security.Permission; +import java.security.PermissionCollection; +import java.security.Permissions; +import java.security.Policy; +import java.security.URIParameter; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.PropertyPermission; +import java.util.Set; import net.sourceforge.jnlp.config.DeploymentConfiguration; import net.sourceforge.jnlp.runtime.JNLPRuntime; @@ -90,7 +104,7 @@ * The HTML permission level corresponding to the given String. If null is given, null comes * back. If there is no permission level that can be granted in HTML matching the given String, * null is also returned. - * @param jnlpString the JNLP permission String + * @param htmlString the JNLP permission String * @return the matching RequestedPermissionLevel */ public RequestedPermissionLevel fromHtmlString(final String htmlString) { @@ -129,10 +143,44 @@ private final boolean grantAwtPermissions; /** the JNLP file */ - private JNLPFile file; + private final JNLPFile file; private final Policy customTrustedPolicy; + /** + * URLPermission is new in Java 8, so we use reflection to check for it to keep compatibility + * with Java 6/7. If we can't find the class or fail to construct it then we continue as usual + * without. + * + * These are saved as fields so that the reflective lookup only needs to be performed once + * when the SecurityDesc is constructed, rather than every time a call is made to + * {@link SecurityDesc#getSandBoxPermissions()}, which is called frequently. + */ + private static Class urlPermissionClass = null; + private static Constructor urlPermissionConstructor = null; + + static { + try { + urlPermissionClass = (Class) Class.forName("java.net.URLPermission"); + urlPermissionConstructor = urlPermissionClass.getDeclaredConstructor(new Class[] { String.class }); + } catch (final SecurityException e) { + OutputController.getLogger().log(OutputController.Level.WARNING_DEBUG, "Exception while reflectively finding URLPermission - host is probably not running Java 8+"); + OutputController.getLogger().log(OutputController.Level.WARNING_DEBUG, e); + urlPermissionClass = null; + urlPermissionConstructor = null; + } catch (final ClassNotFoundException e) { + OutputController.getLogger().log(OutputController.Level.WARNING_DEBUG, "Exception while reflectively finding URLPermission - host is probably not running Java 8+"); + OutputController.getLogger().log(OutputController.Level.WARNING_DEBUG, e); + urlPermissionClass = null; + urlPermissionConstructor = null; + } catch (final NoSuchMethodException e) { + OutputController.getLogger().log(OutputController.Level.WARNING_DEBUG, "Exception while reflectively finding URLPermission - host is probably not running Java 8+"); + OutputController.getLogger().log(OutputController.Level.WARNING_DEBUG, e); + urlPermissionClass = null; + urlPermissionConstructor = null; + } + } + // We go by the rules here: // http://java.sun.com/docs/books/tutorial/deployment/doingMoreWithRIA/properties.html @@ -321,8 +369,7 @@ * Returns a PermissionCollection containing the sandbox permissions */ public PermissionCollection getSandBoxPermissions() { - - Permissions permissions = new Permissions(); + final Permissions permissions = new Permissions(); for (int i = 0; i < sandboxPermissions.length; i++) permissions.add(sandboxPermissions[i]); @@ -345,9 +392,117 @@ permissions.add(new SocketPermission(downloadHost, "connect, accept")); + final Collection urlPermissions = getUrlPermissions(); + for (final Permission permission : urlPermissions) { + permissions.add(permission); + } + return permissions; } - + + private Set getUrlPermissions() { + if (urlPermissionClass == null || urlPermissionConstructor == null) { + return Collections.emptySet(); + } + final Set permissions = new HashSet(); + for (final JARDesc jar : file.getResources().getJARs()) { + try { + // Allow applets all HTTP methods (ex POST, GET) with any request headers + // on resources anywhere recursively in or below the applet codebase, only on + // default ports and ports explicitly specified in resource locations + final URI resourceLocation = jar.getLocation().toURI().normalize(); + final URI host = getHost(resourceLocation); + final String hostUriString = host.toString(); + final String urlPermissionUrlString = appendRecursiveSubdirToCodebaseHostString(hostUriString); + final Permission p = urlPermissionConstructor.newInstance(urlPermissionUrlString); + permissions.add(p); + } catch (final URISyntaxException e) { + OutputController.getLogger().log(OutputController.Level.WARNING_DEBUG, "Could not determine codebase host for resource at " + jar.getLocation() + " while generating URLPermissions"); + OutputController.getLogger().log(e); + } catch (final InvocationTargetException e) { + OutputController.getLogger().log(OutputController.Level.WARNING_DEBUG, "Exception while attempting to reflectively generate a URLPermission, probably not running on Java 8+?"); + OutputController.getLogger().log(OutputController.Level.WARNING_DEBUG, e); + } catch (final InstantiationException e) { + OutputController.getLogger().log(OutputController.Level.WARNING_DEBUG, "Exception while attempting to reflectively generate a URLPermission, probably not running on Java 8+?"); + OutputController.getLogger().log(OutputController.Level.WARNING_DEBUG, e); + } catch (final IllegalAccessException e) { + OutputController.getLogger().log(OutputController.Level.WARNING_DEBUG, "Exception while attempting to reflectively generate a URLPermission, probably not running on Java 8+?"); + OutputController.getLogger().log(OutputController.Level.WARNING_DEBUG, e); + } + } + try { + final URI codebase = file.getCodeBase().toURI().normalize(); + final URI host = getHost(codebase); + final String codebaseHostUriString = host.toString(); + final String urlPermissionUrlString = appendRecursiveSubdirToCodebaseHostString(codebaseHostUriString); + final Permission p = urlPermissionConstructor.newInstance(urlPermissionUrlString); + permissions.add(p); + } catch (final URISyntaxException e) { + OutputController.getLogger().log(OutputController.Level.WARNING_DEBUG, "Could not determine codebase host for codebase " + file.getCodeBase() + " while generating URLPermissions"); + OutputController.getLogger().log(e); + } catch (final InvocationTargetException e) { + OutputController.getLogger().log(OutputController.Level.WARNING_DEBUG, "Exception while attempting to reflectively generate a URLPermission, probably not running on Java 8+?"); + OutputController.getLogger().log(OutputController.Level.WARNING_DEBUG, e); + } catch (final InstantiationException e) { + OutputController.getLogger().log(OutputController.Level.WARNING_DEBUG, "Exception while attempting to reflectively generate a URLPermission, probably not running on Java 8+?"); + OutputController.getLogger().log(OutputController.Level.WARNING_DEBUG, e); + } catch (final IllegalAccessException e) { + OutputController.getLogger().log(OutputController.Level.WARNING_DEBUG, "Exception while attempting to reflectively generate a URLPermission, probably not running on Java 8+?"); + OutputController.getLogger().log(OutputController.Level.WARNING_DEBUG, e); + } + return permissions; + } + + /** + * Gets the host domain part of an applet's codebase. Removes path, query, and fragment, but preserves scheme, + * user info, and host. The port used is overridden with the specified port. + * @param codebase the applet codebase URL + * @param port + * @return the host domain of the codebase + * @throws URISyntaxException + */ + static URI getHostWithSpecifiedPort(final URI codebase, final int port) throws URISyntaxException { + requireNonNull(codebase); + return new URI(codebase.getScheme(), codebase.getUserInfo(), codebase.getHost(), port, null, null, null); + } + + /** + * Gets the host domain part of an applet's codebase. Removes path, query, and fragment, but preserves scheme, + * user info, host, and port. + * @param codebase the applet codebase URL + * @return the host domain of the codebase + * @throws URISyntaxException + */ + static URI getHost(final URI codebase) throws URISyntaxException { + requireNonNull(codebase); + return getHostWithSpecifiedPort(codebase, codebase.getPort()); + } + + /** + * Appends a recursive access marker to a codebase host, for granting Java 8 URLPermissions which are no + * more restrictive than the existing SocketPermissions + * See http://docs.oracle.com/javase/8/docs/api/java/net/URLPermission.html + * @param codebaseHost the applet's codebase's host domain URL as a String. Expected to be formatted as eg + * "http://example.com:8080" or "http://example.com/" + * @return the resulting String eg "http://example.com:8080/- + */ + static String appendRecursiveSubdirToCodebaseHostString(final String codebaseHost) { + requireNonNull(codebaseHost); + String result = codebaseHost; + while (result.endsWith("/")) { + result = result.substring(0, result.length() - 1); + } + // See http://docs.oracle.com/javase/8/docs/api/java/net/URLPermission.html + result = result + "/-"; // allow access to any resources recursively on the host domain + return result; + } + + private static void requireNonNull(final Object arg) { + if (arg == null) { + throw new NullPointerException(); + } + } + /** * Returns all the names of the basic JNLP system properties accessible by RIAs */ diff -r 39631aab56cf -r 36ecc6fe6f62 tests/netx/unit/net/sourceforge/jnlp/SecurityDescTest.java --- a/tests/netx/unit/net/sourceforge/jnlp/SecurityDescTest.java Thu Jul 31 09:19:59 2014 -0400 +++ b/tests/netx/unit/net/sourceforge/jnlp/SecurityDescTest.java Thu Jul 31 16:57:39 2014 -0400 @@ -36,35 +36,173 @@ */ package net.sourceforge.jnlp; +import java.net.URI; import net.sourceforge.jnlp.mock.DummyJNLPFile; -import org.junit.Assert; import org.junit.Test; +import static org.junit.Assert.*; + public class SecurityDescTest { @Test - public void testNotNullJnlpFile() { + public void testNotNullJnlpFile() throws Exception { Throwable t = null; try { - SecurityDesc securityDesc = new SecurityDesc(new DummyJNLPFile(), SecurityDesc.SANDBOX_PERMISSIONS, "hey!"); + new SecurityDesc(new DummyJNLPFile(), SecurityDesc.SANDBOX_PERMISSIONS, "hey!"); } catch (Exception ex) { t = ex; } - Assert.assertNull("securityDesc should not throw exception", t); + assertNull("securityDesc should not throw exception", t); + } + + @Test(expected = NullPointerException.class) + public void testNullJnlpFile() throws Exception { + new SecurityDesc(null, SecurityDesc.SANDBOX_PERMISSIONS, "hey!"); + } + + @Test + public void testAppendRecursiveSubdirToCodebaseHostString() throws Exception { + final String urlStr = "http://example.com"; + final String result = SecurityDesc.appendRecursiveSubdirToCodebaseHostString(urlStr); + final String expected = "http://example.com/-"; + assertEquals(expected, result); + } + + @Test + public void testAppendRecursiveSubdirToCodebaseHostString2() throws Exception { + final String urlStr = "http://example.com/"; + final String result = SecurityDesc.appendRecursiveSubdirToCodebaseHostString(urlStr); + final String expected = "http://example.com/-"; + assertEquals(expected, result); + } + + @Test + public void testAppendRecursiveSubdirToCodebaseHostString3() throws Exception { + final String urlStr = "http://example.com///"; + final String result = SecurityDesc.appendRecursiveSubdirToCodebaseHostString(urlStr); + final String expected = "http://example.com/-"; + assertEquals(expected, result); + } + @Test + public void testAppendRecursiveSubdirToCodebaseHostStringWithPort() throws Exception { + final String urlStr = "http://example.com:8080"; + final String result = SecurityDesc.appendRecursiveSubdirToCodebaseHostString(urlStr); + final String expected = "http://example.com:8080/-"; + assertEquals(expected, result); + } + @Test(expected = NullPointerException.class) + public void testAppendRecursiveSubdirToCodebaseHostStringWithNull() throws Exception { + SecurityDesc.appendRecursiveSubdirToCodebaseHostString(null); + } + + @Test + public void testGetHostWithSpecifiedPort() throws Exception { + final URI codebase = new URI("http://example.com"); + final URI expected = new URI("http://example.com:80"); + assertEquals(expected, SecurityDesc.getHostWithSpecifiedPort(codebase, 80)); + } + + @Test + public void testGetHostWithSpecifiedPortWithFtpScheme() throws Exception { + final URI codebase = new URI("ftp://example.com"); + final URI expected = new URI("ftp://example.com:80"); + assertEquals(expected, SecurityDesc.getHostWithSpecifiedPort(codebase, 80)); + } + + @Test + public void testGetHostWithSpecifiedPortWithUserInfoWi() throws Exception { + final URI codebase = new URI("http://user:password@example.com"); + final URI expected = new URI("http://user:password@example.com:80"); + assertEquals(expected, SecurityDesc.getHostWithSpecifiedPort(codebase, 80)); + } + + @Test + public void testGetHostWithSpecifiedPortWithPort() throws Exception { + final URI codebase = new URI("http://example.com:8080"); + final URI expected = new URI("http://example.com:80"); + assertEquals(expected, SecurityDesc.getHostWithSpecifiedPort(codebase, 80)); } @Test - public void testNullJnlpFile() { - Exception ex = null; - try { - SecurityDesc securityDesc = new SecurityDesc(null, SecurityDesc.SANDBOX_PERMISSIONS, "hey!"); - } catch (Exception eex) { - ex = eex; - } - Assert.assertNotNull("Exception should not be null", ex); - Assert.assertTrue("Exception should be " + NullJnlpFileException.class.getName(), ex instanceof NullJnlpFileException); + public void testGetHostWithSpecifiedPortWithPath() throws Exception { + final URI codebase = new URI("http://example.com/applet/codebase/"); + final URI expected = new URI("http://example.com:80"); + assertEquals(expected, SecurityDesc.getHostWithSpecifiedPort(codebase, 80)); + } + + @Test + public void testGetHostWithSpecifiedPortWithAll() throws Exception { + final URI codebase = new URI("ftp://user:password@example.com:8080/applet/codebase/"); + final URI expected = new URI("ftp://user:password@example.com:80"); + assertEquals(expected, SecurityDesc.getHostWithSpecifiedPort(codebase, 80)); + } + + @Test(expected = NullPointerException.class) + public void testGetHostWithSpecifiedPortWithNull() throws Exception { + SecurityDesc.getHostWithSpecifiedPort(null, 80); + } + + @Test + public void testGetHost() throws Exception { + final URI codebase = new URI("http://example.com"); + final URI expected = new URI("http://example.com"); + assertEquals(expected, SecurityDesc.getHost(codebase)); + } + + @Test + public void testGetHostWithFtpScheme() throws Exception { + final URI codebase = new URI("ftp://example.com"); + final URI expected = new URI("ftp://example.com"); + assertEquals(expected, SecurityDesc.getHost(codebase)); + } + + @Test + public void testGetHostWithUserInfo() throws Exception { + final URI codebase = new URI("http://user:password@example.com"); + final URI expected = new URI("http://user:password@example.com"); + assertEquals(expected, SecurityDesc.getHost(codebase)); + } + @Test + public void testGetHostWithPort() throws Exception { + final URI codebase = new URI("http://example.com:8080"); + final URI expected = new URI("http://example.com:8080"); + assertEquals(expected, SecurityDesc.getHost(codebase)); } + + @Test + public void testGetHostWithPath() throws Exception { + final URI codebase = new URI("http://example.com/applet/codebase/"); + final URI expected = new URI("http://example.com"); + assertEquals(expected, SecurityDesc.getHost(codebase)); + } + + @Test + public void testGetHostWithAll() throws Exception { + final URI codebase = new URI("ftp://user:password@example.com:8080/applet/codebase/"); + final URI expected = new URI("ftp://user:password@example.com:8080"); + assertEquals(expected, SecurityDesc.getHost(codebase)); + } + + @Test(expected = NullPointerException.class) + public void testGetHostNull() throws Exception { + SecurityDesc.getHost(null); + } + + @Test + public void testGetHostWithAppendRecursiveSubdirToCodebaseHostString() throws Exception { + final URI codebase = new URI("ftp://user:password@example.com:8080/applet/codebase/"); + final String expected = "ftp://user:password@example.com:8080/-"; + assertEquals(expected, SecurityDesc.appendRecursiveSubdirToCodebaseHostString(SecurityDesc.getHost(codebase).toString())); + } + + @Test + public void testGetHostWithSpecifiedPortWithAppendRecursiveSubdirToCodebaseHostString() throws Exception { + final URI codebase = new URI("ftp://user:password@example.com:8080/applet/codebase/"); + final String expected = "ftp://user:password@example.com:80/-"; + assertEquals(expected, SecurityDesc.appendRecursiveSubdirToCodebaseHostString(SecurityDesc.getHostWithSpecifiedPort(codebase, 80).toString())); + } + }