// Copyright (C) 2001-2003 Jon A. Maxwell (JAM) // // This library is free software; you can redistribute it and/or // modify it under the terms of the GNU Lesser General Public // License as published by the Free Software Foundation; either // version 2.1 of the License, or (at your option) any later version. // // This library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. package net.sourceforge.jnlp.runtime; import static net.sourceforge.jnlp.runtime.Translator.R; import java.awt.Frame; import java.awt.Window; import java.lang.ref.WeakReference; import java.net.SocketPermission; import java.security.AllPermission; import java.security.AccessControlException; import java.security.Permission; import java.security.SecurityPermission; import javax.swing.JWindow; import net.sourceforge.jnlp.JNLPFile; import net.sourceforge.jnlp.security.SecurityDialogs.AccessType; import net.sourceforge.jnlp.services.ServiceUtil; import net.sourceforge.jnlp.util.WeakList; import sun.awt.AWTSecurityManager; import sun.awt.AppContext; import sun.security.util.SecurityConstants; /** * Security manager for JNLP environment. This security manager * cannot be replaced as it always denies attempts to replace the * security manager or policy.

* * The JNLP security manager tracks windows created by an * application, allowing those windows to be disposed when the * application exits but the JVM does not. If security is not * enabled then the first application to call System.exit will * halt the JVM.

* * @author Jon A. Maxwell (JAM) - initial author * @version $Revision: 1.17 $ */ class JNLPSecurityManager extends AWTSecurityManager { // todo: some apps like JDiskReport can close the VM even when // an exit class is set - fix! // todo: create an event dispatch thread for each application, // so that the context classloader doesn't have to be switched // to the foreground application (the currently the approach // since some apps need their classloader as event dispatch // thread's context classloader). // todo: use a custom Permission object to identify the current // application in an AccessControlContext by setting a side // effect in its implies method. Use a custom // AllPermissions-like permission to do this for apps granted // all permissions (but investigate whether this will nuke // the all-permission optimizations in the JRE). // todo: does not exit app if close button pressed on JFrame // with CLOSE_ON_EXIT (or whatever) set; if doesn't exit, use an // WindowListener to catch WindowClosing event, then if exit is // called immediately afterwards from AWT thread. // todo: deny all permissions to applications that should have // already been 'shut down' by closing their resources and // interrupt the threads if operating in a shared-VM (exit class // set). Deny will probably will slow checks down a lot though. // todo: weak remember last getProperty application and // re-install properties if another application calls, or find // another way for different apps to have different properties // in java.lang.Sytem with the same names. /** only class that can exit the JVM, if set */ private Object exitClass = null; /** this exception prevents exiting the JVM */ private SecurityException closeAppEx = // making here prevents huge stack traces new SecurityException(R("RShutdown")); /** weak list of windows created */ private WeakList weakWindows = new WeakList(); /** weak list of applications corresponding to window list */ private WeakList weakApplications = new WeakList(); /** Sets whether or not exit is allowed (in the context of the plugin, this is always false) */ private boolean exitAllowed = true; /** * The AppContext of the main application (netx). We need to store this here * so we can return this when no code from an external application is * running on the thread */ private AppContext mainAppContext; /** * Creates a JNLP SecurityManager. */ JNLPSecurityManager() { // this has the side-effect of creating the Swing shared Frame // owner. Since no application is running at this time, it is // not added to any window list when checkTopLevelWindow is // called for it (and not disposed). if (!JNLPRuntime.isHeadless()) new JWindow().getOwner(); mainAppContext = AppContext.getAppContext(); } /** * Returns whether the exit class is present on the stack, or * true if no exit class is set. */ public boolean isExitClass() { return isExitClass(getClassContext()); } /** * Returns whether the exit class is present on the stack, or * true if no exit class is set. */ private boolean isExitClass(Class stack[]) { if (exitClass == null) return true; for (int i = 0; i < stack.length; i++) if (stack[i] == exitClass) return true; return false; } /** * Set the exit class, which is the only class that can exit the * JVM; if not set then any class can exit the JVM. * * @param exitClass the exit class * @throws IllegalStateException if the exit class is already set */ public void setExitClass(Class exitClass) throws IllegalStateException { if (this.exitClass != null) throw new IllegalStateException(R("RExitTaken")); this.exitClass = exitClass; } /** * Return the current Application, or null if none can be * determined. */ protected ApplicationInstance getApplication() { return getApplication(getClassContext(), 0); } /** * Return the application the opened the specified window (only * call from event dispatch thread). */ protected ApplicationInstance getApplication(Window window) { for (int i = weakWindows.size(); i-- > 0;) { Window w = weakWindows.get(i); if (w == null) { weakWindows.remove(i); weakApplications.remove(i); } if (w == window) return weakApplications.get(i); } return null; } /** * Return the current Application, or null. */ protected ApplicationInstance getApplication(Class stack[], int maxDepth) { if (maxDepth <= 0) maxDepth = stack.length; // this needs to be tightened up for (int i = 0; i < stack.length && i < maxDepth; i++) { ClassLoader cl = stack[i].getClassLoader(); // Since we want to deal with JNLPClassLoader, extract it if this // is a codebase loader if (cl instanceof JNLPClassLoader.CodeBaseClassLoader) cl = ((JNLPClassLoader.CodeBaseClassLoader) cl).getParentJNLPClassLoader(); if (cl instanceof JNLPClassLoader) { JNLPClassLoader loader = (JNLPClassLoader) cl; if (loader != null && loader.getApplication() != null) { return loader.getApplication(); } } } return null; } /** * Returns the application's thread group if the application can * be determined; otherwise returns super.getThreadGroup() */ public ThreadGroup getThreadGroup() { ApplicationInstance app = getApplication(); if (app == null) return super.getThreadGroup(); return app.getThreadGroup(); } /** * Throws a SecurityException if the permission is denied, * otherwise return normally. This method always denies * permission to change the security manager or policy. */ public void checkPermission(Permission perm) { String name = perm.getName(); // Enable this manually -- it'll produce too much output for -verbose // otherwise. // if (true) // System.out.println("Checking permission: " + perm.toString()); if (!JNLPRuntime.isWebstartApplication() && ("setPolicy".equals(name) || "setSecurityManager".equals(name))) throw new SecurityException(R("RCantReplaceSM")); try { // deny all permissions to stopped applications // The call to getApplication() below might not work if an // application hasn't been fully initialized yet. // if (JNLPRuntime.isDebug()) { // if (!"getClassLoader".equals(name)) { // ApplicationInstance app = getApplication(); // if (app != null && !app.isRunning()) // throw new SecurityException(R("RDenyStopped")); // } // } try { super.checkPermission(perm); } catch (SecurityException se) { //This section is a special case for dealing with SocketPermissions. if (JNLPRuntime.isDebug()) System.err.println("Requesting permission: " + perm.toString()); //Change this SocketPermission's action to connect and accept //(and resolve). This is to avoid asking for connect permission //on every address resolve. Permission tmpPerm = null; if (perm instanceof SocketPermission) { tmpPerm = new SocketPermission(perm.getName(), SecurityConstants.SOCKET_CONNECT_ACCEPT_ACTION); // before proceeding, check if we are trying to connect to same origin ApplicationInstance app = getApplication(); JNLPFile file = app.getJNLPFile(); String srcHost = file.getSourceLocation().getAuthority(); String destHost = name; // host = abc.xyz.com or abc.xyz.com: if (destHost.indexOf(':') >= 0) destHost = destHost.substring(0, destHost.indexOf(':')); // host = abc.xyz.com String[] hostComponents = destHost.split("\\."); int length = hostComponents.length; if (length >= 2) { // address is in xxx.xxx.xxx format destHost = hostComponents[length - 2] + "." + hostComponents[length - 1]; // host = xyz.com i.e. origin boolean isDestHostName = false; // make sure that it is not an ip address try { Integer.parseInt(hostComponents[length - 1]); } catch (NumberFormatException e) { isDestHostName = true; } if (isDestHostName) { // okay, destination is hostname. Now figure out if it is a subset of origin if (srcHost.endsWith(destHost)) { addPermission(tmpPerm); return; } } } } else { tmpPerm = perm; } if (tmpPerm != null) { //askPermission will only prompt the user on SocketPermission //meaning we're denying all other SecurityExceptions that may arise. if (askPermission(tmpPerm)) { addPermission(tmpPerm); //return quietly. } else { throw se; } } } } catch (SecurityException ex) { if (JNLPRuntime.isDebug()) { System.out.println("Denying permission: " + perm); } throw ex; } } /** * Asks the user whether or not to grant permission. * @param perm the permission to be granted * @return true if the permission was granted, false otherwise. */ private boolean askPermission(Permission perm) { ApplicationInstance app = getApplication(); if (app != null && !app.isSigned()) { if (perm instanceof SocketPermission && ServiceUtil.checkAccess(AccessType.NETWORK, perm.getName())) { return true; } } return false; } /** * Adds a permission to the JNLPClassLoader. * @param perm the permission to add to the JNLPClassLoader */ private void addPermission(Permission perm) { if (JNLPRuntime.getApplication().getClassLoader() instanceof JNLPClassLoader) { JNLPClassLoader cl = (JNLPClassLoader) JNLPRuntime.getApplication().getClassLoader(); cl.addPermission(perm); if (JNLPRuntime.isDebug()) { if (cl.getPermissions(null).implies(perm)) System.err.println("Added permission: " + perm.toString()); else System.err.println("Unable to add permission: " + perm.toString()); } } else { if (JNLPRuntime.isDebug()) System.err.println("Unable to add permission: " + perm + ", classloader not JNLP."); } } /** * Checks whether the window can be displayed without an applet * warning banner, and adds the window to the list of windows to * be disposed when the calling application exits. */ public boolean checkTopLevelWindow(Object window) { ApplicationInstance app = getApplication(); // remember window -> application mapping for focus, close on exit if (app != null && window instanceof Window) { Window w = (Window) window; if (JNLPRuntime.isDebug()) System.err.println("SM: app: " + app.getTitle() + " is adding a window: " + window + " with appContext " + AppContext.getAppContext()); weakWindows.add(w); // for mapping window -> app weakApplications.add(app); app.addWindow(w); } // change coffee cup to netx for default icon if (window instanceof Window) for (Window w = (Window) window; w != null; w = w.getOwner()) if (window instanceof Frame) ((Frame) window).setIconImage(JNLPRuntime.getWindowIcon()); // todo: set awt.appletWarning to custom message // todo: logo on with glass pane on JFrame/JWindow? return super.checkTopLevelWindow(window); } /** * Checks whether the caller can exit the system. This method * identifies whether the caller is a real call to Runtime.exec * and has special behavior when returning from this method * would exit the JVM and an exit class is set: if the caller is * not the exit class then the calling application will be * stopped and its resources destroyed (when possible), and an * exception will be thrown to prevent the JVM from shutting * down.

* * Calls not from Runtime.exit or with no exit class set will * behave normally, and the exit class can always exit the JVM. */ public void checkExit(int status) { // applets are not allowed to exit, but the plugin main class (primordial loader) is Class stack[] = getClassContext(); if (!exitAllowed) { for (int i = 0; i < stack.length; i++) if (stack[i].getClassLoader() != null) throw new AccessControlException("Applets may not call System.exit()"); } super.checkExit(status); boolean realCall = (stack[1] == Runtime.class); if (isExitClass(stack)) // either exitClass called or no exitClass set return; // to Runtime.exit or fake call to see if app has permission // not called from Runtime.exit() if (!realCall) { // apps that can't exit should think they can exit normally super.checkExit(status); return; } // but when they really call, stop only the app instead of the JVM ApplicationInstance app = getApplication(stack, 0); if (app == null) { throw new SecurityException(R("RExitNoApp")); } app.destroy(); throw closeAppEx; } protected void disableExit() { exitAllowed = false; } /** * This returns the appropriate {@link AppContext}. Hooks in AppContext * check if the current {@link SecurityManager} is an instance of * AWTSecurityManager and if so, call this method to give it a chance to * return the appropriate appContext based on the application that is * running.

* * This can be called from any thread (possibly a swing thread) to find out * the AppContext for the thread (which may correspond to a particular * applet). */ @Override public AppContext getAppContext() { ApplicationInstance app = getApplication(); if (app == null) { /* * if we cannot find an application based on the code on the stack, * then assume it is the main application */ return mainAppContext; } else { return app.getAppContext(); } } /** * Tests if a client can get access to the AWT event queue. This version allows * complete access to the EventQueue for its own AppContext-specific EventQueue. * * FIXME there are probably huge security implications for this. Eg: * http://hg.openjdk.java.net/jdk7/awt/jdk/rev/8022709a306d * * @exception SecurityException if the caller does not have * permission to accesss the AWT event queue. */ public void checkAwtEventQueueAccess() { /* * this is the templace of the code that should allow applets access to * eventqueues */ // AppContext appContext = AppContext.getAppContext(); // ApplicationInstance instance = getApplication(); // if ((appContext == mainAppContext) && (instance != null)) { // If we're about to allow access to the main EventQueue, // and anything untrusted is on the class context stack, // disallow access. super.checkAwtEventQueueAccess(); // } } }