changeset 1006:36ecc6fe6f62

URLPermissions granted in SecurityDesc if available 2014-07-31 Andrew Azores <aazores@redhat.com> 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
author Andrew Azores <aazores@redhat.com>
date Thu, 31 Jul 2014 16:57:39 -0400
parents 39631aab56cf
children 247a09f2cd10
files ChangeLog NEWS netx/net/sourceforge/jnlp/SecurityDesc.java tests/netx/unit/net/sourceforge/jnlp/SecurityDescTest.java
diffstat 4 files changed, 349 insertions(+), 22 deletions(-) [+]
line wrap: on
line diff
--- 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  <aazores@redhat.com>
+
+	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  <aazores@redhat.com>
 
 	Fixes for coverity issues discovered in RH1121549
--- 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
--- 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<Permission> urlPermissionClass = null;
+    private static Constructor<Permission> urlPermissionConstructor = null;
+
+    static {
+        try {
+            urlPermissionClass = (Class<Permission>) 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<Permission> urlPermissions = getUrlPermissions();
+        for (final Permission permission : urlPermissions) {
+            permissions.add(permission);
+        }
+
         return permissions;
     }
-    
+
+    private Set<Permission> getUrlPermissions() {
+        if (urlPermissionClass == null || urlPermissionConstructor == null) {
+            return Collections.emptySet();
+        }
+        final Set<Permission> permissions = new HashSet<Permission>();
+        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
      */
--- 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()));
+    }
+
 }