/*
* 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 com.sun.jini.start;
import net.jini.config.Configuration;
import net.jini.config.ConfigurationException;
import net.jini.export.ProxyAccessor;
import net.jini.id.Uuid;
import net.jini.id.UuidFactory;
import net.jini.loader.pref.PreferredClassLoader;
import net.jini.security.policy.DynamicPolicy;
import net.jini.security.policy.DynamicPolicyProvider;
import net.jini.security.policy.PolicyFileProvider;
import java.io.InvalidObjectException;
import java.io.ObjectStreamException;
import java.io.PrintWriter;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.rmi.MarshalException;
import java.rmi.MarshalledObject;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.StubNotFoundException;
import java.rmi.UnmarshalException;
import java.rmi.activation.Activatable;
import java.rmi.activation.ActivationDesc;
import java.rmi.activation.ActivationException;
import java.rmi.activation.ActivationGroup;
import java.rmi.activation.ActivationGroupID;
import java.rmi.activation.ActivationID;
import java.rmi.activation.ActivationSystem;
import java.rmi.server.RMIClassLoader;
import java.rmi.server.RemoteObject;
import java.rmi.server.RemoteRef;
import java.security.AccessController;
import java.security.AllPermission;
import java.security.cert.Certificate;
import java.security.CodeSource;
import java.security.Permission;
import java.security.PermissionCollection;
import java.security.Permissions;
import java.security.Policy;
import java.security.Principal;
import java.security.PrivilegedAction;
import java.security.ProtectionDomain;
import java.security.Security;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.WeakHashMap;
/**
* A wrapper for activatable objects, providing separation of the import
* codebase (where the server classes are loaded from by the activation
* group) from the export codebase (where clients should load classes from
* for stubs, etc.) as well as providing an independent security policy file
* for each activatable object. This functionality allows multiple
* activatable objects to be placed in the same activation group, with each
* object maintaining a distinct codebase and policy.
* <p>
* This wrapper class is assumed to be available directly in the activation
* group VM; that is, it is assumed to be in the application classloader,
* the extension classloader, or the boot classloader, rather than being
* downloaded. Since this class also needs considerable permissions, the
* easiest thing to do is to make it an installed extension.
* <p>
* This wrapper class performs a security check to control what
* policy files can be used with a given codebase.
* It does this by querying the VM's (global) policy for
* {@link com.sun.jini.start.SharedActivationPolicyPermission}
* grants. The service's associated
* {@link com.sun.jini.start.ActivateWrapper.ActivateDesc#importLocation
* ActivateDesc.importLocation} is used as
* the {@link java.security.CodeSource}
* for selecting the appropriate permission set to
* check against. If multiple codebases are used, then all the codebases must
* have the necessary <code>SharedActivationPolicyPermission</code> grants.
* <p>
* An example of how to use this wrapper:
* <pre>
* URL[] importURLs = new URL[] {new URL("http://myhost:8080/service.jar")};
* URL[] exportURLs = new URL[] {new URL("http://myhost:8080/service-dl.jar")};
* ActivationID aid
* = ActivateWrapper.register(
* gid,
* new ActivateWrapper.ActivateDesc(
* "foo.bar.ServiceImpl",
* importURLs,
* exportURLs,
* "http://myhost:8080/service.policy",
* new MarshalledObject(
* new String[] { "/tmp/service.config" })
* ),
* true,
* activationSystem);
* </pre>
* <A NAME="serviceConstructor">
* Clients of this wrapper service need to implement the following "activation
* constructor":
* <blockquote><pre>
* <impl>(ActivationID activationID, MarshalledObject data)
* </blockquote></pre>
* where,
* <UL>
* <LI>activationID - is the object's activation identifier
* <LI>data - is the object's activation data
* </UL>
*
* Clients of this wrapper service can also implement
* {@link net.jini.export.ProxyAccessor}, which allows the service
* implementation to provide a remote reference of its choosing.
* <P>
* <A NAME="configEntries">
* This implementation of <code>ActivateWrapper</code>
* supports the
* following {@link java.security.Security} property:
*
* <table summary="Describes the com.sun.jini.start.servicePolicyProvider
* security property"
* border="0" cellpadding="2">
* <tr valign="top">
* <th scope="col" summary="layout"> <font size="+1">•</font>
* <th scope="col" align="left" colspan="2"> <font size="+1"><code>
* com.sun.jini.start.servicePolicyProvider</code></font>
* <tr valign="top"> <td>   <th scope="row" align="right">
* Default: <td> <code>
* "net.jini.security.policy.DynamicPolicyProvider"
* </code>
* <tr valign="top"> <td>   <th scope="row" align="right">
* Description: <td> The fully qualified class name of a
* dynamic policy provider (see {@link net.jini.security.policy.DynamicPolicy})
* which will be used to "wrap" all service policy files.
* The implementation class needs to:
* <UL>
* <LI> implement the following constructor:
* <blockquote><pre>
* public <impl>(Policy servicePolicy)
* </blockquote></pre>
* where,
* <UL>
* <LI>servicePolicy - is the service policy object to be wrapped
* </UL>
* <LI> implement {@link net.jini.security.policy.DynamicPolicy}
* <LI> be a public, non-interface, non-abstract class
* </UL>
*
* <P>
* A custom service policy provider can be very useful when trying to
* debug security related issues.
* <code>com.sun.jini.tool.DebugDynamicPolicyProvider</code> is an example
* policy provider that provides this functionality and can be located
* via the following URL:
* <A HREF="http://starterkit-examples.jini.org/">
* http://starterkit-examples.jini.org/
* </A><BR>
* <I>Note:</I>The custom policy implementation is assumed to be
* available from the system classloader of the virtual machine
* hosting the service. Its codebase should also be granted
* {@link java.security.AllPermission}.
* </table>
*
* @see com.sun.jini.start.SharedActivationPolicyPermission
* @see java.rmi.activation.ActivationID
* @see java.rmi.MarshalledObject
* @see java.rmi.Remote
* @see java.security.CodeSource
* @see net.jini.export.ProxyAccessor
*
* @author Sun Microsystems, Inc.
*
*/
public class ActivateWrapper implements Remote, Serializable {
/** Configure logger */
static final Logger logger = Logger.getLogger("com.sun.jini.start.wrapper");
/**
* The <code>Policy</code> object that aggregates the individual
* service policy objects.
*/
private static AggregatePolicyProvider globalPolicy;
/**
* The <code>Policy</code> object in effect at startup.
*/
private static Policy initialGlobalPolicy;
/**
* The "wrapped" activatable object.
* @serial
*/
private /*final*/ Object impl;
/**
* The parameter types for the "activation constructor".
*/
private static final Class[] actTypes = {
ActivationID.class, MarshalledObject.class
};
/**
* Fully qualified name of custom, service policy provider
*/
private static String servicePolicyProvider =
((String) AccessController.doPrivileged(
new PrivilegedAction() {
public Object run() {
return Security.getProperty(
"com.sun.jini.start." +
"servicePolicyProvider");
}
}));
/**
* The parameter types for the
* "custom, service policy constructor".
*/
private static final Class[] policyTypes = {
Policy.class
};
/**
* Descriptor for registering a "wrapped" activatable object. This
* descriptor gets stored as the <code>MarshalledObject</code>
* initialization data in the <code>ActivationDesc</code>.
*/
public static class ActivateDesc implements Serializable {
private static final long serialVersionUID = 2L;
/**
* The activatable object's class name.
* @serial
*/
public final String className;
/**
* The codebase where the server classes are loaded from by the
* activation group.
* @serial
*/
public final URL[] importLocation;
/**
* The codebase where clients should load classes from for stubs, etc.
* @serial
*/
public final URL[] exportLocation;
/**
* The security policy filename or URL.
* @serial
*/
public final String policy;
/**
* The activatable object's initialization data.
* @serial
*/
public final MarshalledObject data;
/**
* Trivial constructor.
*/
public ActivateDesc(String className,
URL[] importLocation,
URL[] exportLocation,
String policy,
MarshalledObject data)
{
//TODO - clone non-String objects?
this.className = className;
this.importLocation = importLocation;
this.exportLocation = exportLocation;
this.policy = policy;
this.data = data;
}
// Javadoc inherited from supertype
public String toString() {
return "[className=" + className + ","
+ "importLocation="
+ ((importLocation == null)
? null : Arrays.asList(importLocation))
+ ","
+ "exportLocation="
+ ((exportLocation == null)
? null : Arrays.asList(exportLocation))
+ ","
+ "policy=" + policy + ","
+ "data=" + data + "]";
}
}
/**
* A simple subclass of <code>PreferredClassLoader</code> that overrides
* <code>getURLs</code> to
* return the <code>URL</code>s of the provided export codebase.
* <code>getURLs</code>
* is called by the RMI subsystem in order to annotate objects
* leaving the virtual machine.
*/
/*
* Implementation note. Subclasses of this class that override
* getClassAnnotation might need to override getURLs because getURLs
* uses a "cached" version of the export annotation.
*/
static class ExportClassLoader extends PreferredClassLoader
{
/** Cached value of the provided export codebase <code>URL</code>s */
private final URL[] exportURLs;
/** Id field used to make toString() unique */
private final Uuid id = UuidFactory.generate();
/** Trivial constructor that calls
* <pre>
* super(importURLs, parent, urlsToPath(exportURLs), false);
* </pre>
* and assigns <code>exportURLs</code> to an internal field.
*/
public ExportClassLoader(URL[] importURLs, URL[] exportURLs,
ClassLoader parent)
{
super(importURLs, parent, urlsToPath(exportURLs), false);
// Not safe to call getClassAnnotation() w/i cons if subclassed,
// so need to redo "super" logic here.
if (exportURLs == null) {
this.exportURLs = importURLs;
} else {
this.exportURLs = exportURLs;
}
}
//Javadoc inherited from super type
public URL[] getURLs() {
return (URL[])exportURLs.clone();
}
// Javadoc inherited from supertype
public String toString() {
URL[] urls = super.getURLs();
return this.getClass().getName()
+ "[importURLs="
+ (urls==null?null:Arrays.asList(urls))
+ ","
+ "exportURLs="
+ (exportURLs==null?null:Arrays.asList(exportURLs))
+ ","
+ "parent=" + getParent()
+ ","
+ "id=" + id
+ "]";
}
}
/**
* Activatable constructor. This constructor:
* <UL>
* <LI>Retrieves an <code>ActivateDesc</code> from the
* provided <code>data</code> parameter.
* <LI>creates an <code>ExportClassLoader</code> using the
* import and export codebases obtained from the provided
* <code>ActivateDesc</code>,
* <LI>checks the import codebase(s) for the required
* <code>SharedActivationPolicyPermission</code>
* <LI>associates the newly created <code>ExportClassLoader</code>
* and the corresponding policy file obtained from the
* <code>ActivateDesc</code> with the
* <code>AggregatePolicyProvider</code>
* <LI>loads the "wrapped" activatable object's class and
* calls its activation constructor with the context classloader
* set to the newly created <code>ExportClassLoader</code>.
* <LI> resets the context class loader to the original
* context classloader
* </UL>
* The first instance of this class will also replace the VM's
* existing <code>Policy</code> object, if any,
* with a <code>AggregatePolicyProvider</code>.
*
* @param id The <code>ActivationID</code> of this object
* @param data The activation data for this object
*
* @see com.sun.jini.start.ActivateWrapper.ExportClassLoader
* @see com.sun.jini.start.ActivateWrapper.ActivateDesc
* @see com.sun.jini.start.AggregatePolicyProvider
* @see com.sun.jini.start.SharedActivationPolicyPermission
* @see java.security.Policy
*
*/
public ActivateWrapper(ActivationID id, MarshalledObject data)
throws Exception
{
try {
logger.entering(ActivateWrapper.class.getName(),
"ActivateWrapper", new Object[] { id, data });
ActivateDesc desc = (ActivateDesc)data.get();
logger.log(Level.FINEST, "ActivateDesc: {0}", desc);
Thread t = Thread.currentThread();
ClassLoader ccl = t.getContextClassLoader();
logger.log(Level.FINEST, "Saved current context class loader: {0}",
ccl);
ExportClassLoader cl = null;
try {
cl = new ExportClassLoader(desc.importLocation,
desc.exportLocation,
ccl);
logger.log(Level.FINEST, "Created ExportClassLoader: {0}", cl);
} catch (Exception e) {
logger.throwing(ActivateWrapper.class.getName(),
"ActivateWrapper", e);
throw e;
}
checkPolicyPermission(desc.policy, desc.importLocation);
synchronized (ActivateWrapper.class) {
// supplant global policy 1st time through
if (globalPolicy == null) {
initialGlobalPolicy = Policy.getPolicy();
if (!(initialGlobalPolicy instanceof DynamicPolicy)) {
initialGlobalPolicy =
new DynamicPolicyProvider(initialGlobalPolicy);
}
globalPolicy =
new AggregatePolicyProvider(initialGlobalPolicy);
Policy.setPolicy(globalPolicy);
logger.log(Level.FINEST,
"Global policy set: {0}", globalPolicy);
}
Policy service_policy =
getServicePolicyProvider(
new PolicyFileProvider(desc.policy));
Policy backstop_policy =
getServicePolicyProvider(initialGlobalPolicy);
LoaderSplitPolicyProvider split_service_policy =
new LoaderSplitPolicyProvider(
cl, service_policy, backstop_policy);
/* Grant "this" code enough permission to do its work
* under the service policy, which takes effect (below)
* after the context loader is (re)set.
* Note: Throws UnsupportedOperationException if dynamic grants
* aren't supported (because underlying policies don't support it).
*/
split_service_policy.grant(
this.getClass(),
null, /* Principal[] */
new Permission[] { new AllPermission() } );
globalPolicy.setPolicy(cl, split_service_policy);
logger.log(Level.FINEST,
"Added policy to set: {0}", desc.policy);
}
boolean initialize = false;
Class ac = Class.forName(desc.className, initialize, cl);
logger.log(Level.FINEST, "Obtained implementation class: {0}", ac);
t.setContextClassLoader(cl);
try {
logger.log(Level.FINEST,
"Set new context class loader: {0}", cl);
Constructor constructor =
ac.getDeclaredConstructor(actTypes);
logger.log(Level.FINEST,
"Obtained implementation constructor: {0}",
constructor);
constructor.setAccessible(true);
impl =
constructor.newInstance(new Object[]{id, desc.data});
logger.log(Level.FINEST,
"Obtained implementation instance: {0}", impl);
} finally {
t.setContextClassLoader(ccl);
logger.log(Level.FINEST, "Context class loader reset to: {0}",
ccl);
}
} catch (Exception e) {
logger.throwing(ActivateWrapper.class.getName(),
"ActivateWrapper", e);
throw e;
}
logger.exiting(ActivateWrapper.class.getName(),
"ActivateWrapper");
}
/**
* Return a reference to service being wrapped in place
* of this object.
*/
private Object writeReplace() throws ObjectStreamException {
Object impl_proxy = impl;
if (impl instanceof ProxyAccessor) {
impl_proxy = ((ProxyAccessor) impl).getProxy();
logger.log(Level.FINEST,
"Obtained implementation proxy: {0}", impl_proxy);
if (impl_proxy == null) {
throw new InvalidObjectException(
"Implementation's getProxy() returned null");
}
}
return impl_proxy;
}
/**
* Analog to
* {@link java.rmi.activation.Activatable#register(java.rmi.activation.ActivationDesc)
* Activatable.register()} for activatable objects that want
* to use this wrapper mechanism.
*
* @return activation ID of the registered service
*
* @throws ActivationException if there was a problem registering
* the activatable class with the activation system
* @throws RemoteException if there was a problem communicating
* with the activation system
*/
public static ActivationID register(ActivationGroupID gid,
ActivateDesc desc,
boolean restart,
ActivationSystem sys)
throws ActivationException, RemoteException
{
logger.entering(ActivateWrapper.class.getName(),
"register", new Object[] { gid, desc, Boolean.valueOf(restart), sys });
MarshalledObject data;
try {
data = new MarshalledObject(desc);
} catch (Exception e) {
MarshalException me =
new MarshalException("marshalling ActivateDesc", e);
logger.throwing(ActivateWrapper.class.getName(),
"register", me);
throw me;
}
ActivationDesc adesc =
new ActivationDesc(gid,
ActivateWrapper.class.getName(),
null,
data,
restart
);
logger.log(Level.FINEST,
"Registering descriptor with activation: {0}", adesc);
ActivationID aid = sys.registerObject(adesc);
logger.exiting(ActivateWrapper.class.getName(),
"register", aid);
return aid;
}
/**
* Checks that all the provided <code>URL</code>s have permission to
* use the given policy.
*/
private static void checkPolicyPermission(String policy, URL[] urls) {
logger.entering(ActivateWrapper.class.getName(),
"checkPolicyPermission", new Object[] { policy, urlsToPath(urls) });
// Create desired permission object
Permission perm = new SharedActivationPolicyPermission(policy);
Certificate[] certs = null;
CodeSource cs = null;
ProtectionDomain pd = null;
// Loop over all codebases
for (int i=0; i < urls.length; i++) {
// Create ProtectionDomain for given codesource
cs = new CodeSource(urls[i], certs);
pd = new ProtectionDomain(cs, null, null, null);
logger.log(Level.FINEST,
"Checking protection domain: {0}", pd);
// Check if current domain allows desired permission
if(!pd.implies(perm)) {
SecurityException se = new SecurityException(
"ProtectionDomain " + pd
+ " does not have required permission: " + perm);
logger.throwing(ActivateWrapper.class.getName(),
"checkPolicyPermission", se);
throw se;
}
}
logger.exiting(ActivateWrapper.class.getName(),
"checkPolicyPermission");
}
/**
* Utility method that converts a <code>URL[]</code>
* into a corresponding, space-separated string with
* the same array elements.
*
* Note that if the array has zero elements, the return value is
* null, not the empty string.
*/
private static String urlsToPath(URL[] urls) {
//TODO - check if spaces in file paths are properly escaped (i.e.% chars)
if (urls == null) {
return null;
} else if (urls.length == 0) {
return "";
} else if (urls.length == 1) {
return urls[0].toExternalForm();
} else {
StringBuffer path = new StringBuffer(urls[0].toExternalForm());
for (int i = 1; i < urls.length; i++) {
path.append(' ');
path.append(urls[i].toExternalForm());
}
return path.toString();
}
}
static Policy getServicePolicyProvider(Policy service_policy) throws Exception {
Policy servicePolicyWrapper = null;
if (servicePolicyProvider != null) {
Class sp = Class.forName(servicePolicyProvider);
logger.log(Level.FINEST,
"Obtained custom service policy implementation class: {0}", sp);
Constructor constructor =
sp.getConstructor(policyTypes);
logger.log(Level.FINEST,
"Obtained custom service policy implementation constructor: {0}",
constructor);
servicePolicyWrapper = (Policy)
constructor.newInstance(new Object[]{service_policy});
logger.log(Level.FINEST,
"Obtained custom service policy implementation instance: {0}",
servicePolicyWrapper);
} else {
servicePolicyWrapper = new DynamicPolicyProvider(service_policy);
logger.log(Level.FINEST,
"Using default service policy implementation instance: {0}",
servicePolicyWrapper);
}
return servicePolicyWrapper;
}
}