Remote Method Invocation: Classes, ClassLoader and Security

Example Classes, ClassLoaders, Firewalls,

The latter part of these notes is taken from SUNs Remote Method Invocation Specification

But first, an example of Passing Behavior from the White Paper

Passing Behavior

Here is a description of how you could design an expense report program. We present this to show how you can use RMI's ability to move behavior from one system to another to move computing to where you want it today, and change it easily tomorrow. The examples below do not handle all cases that would arise in the real world, but instead give a flavor for how the problem can be approached.

Server-Defined Policy



Figure 1 shows the general picture of such a dynamically configurable expense reporting system. A client displays a GUI (graphical user interface) to a user, who fills in the fields of the expense report. Clients communicate with the server using RMI. The server stores the expense reports in a database using JDBC, the Java relational database package. So far this may look like any multi-tier system, but there is an important difference-RMI can download behavior.

Suppose that the company's policies about expense reports change. For example, today the company requires receipts only for expenses over $20. Tomorrow the company decides this is too lenient-it wants receipts for everything, except for meals that cost less than $20. Without the ability to download behavior, you have the following alternatives when designing your system for change:

With RMI you can have the client upload behavior from the server with a simple method invocation, providing a flexible way to offload computation from the server to the clients while providing users with faster feedback. When a user is ready to write up a new expense report, the client asks the server for an object that embodies the current policies for expense reports as expressed via a Policy interface written in Java. The object can implement the policy in any way. If this is the first time that the client's RMI runtime has seen this particular implementation of the policy, RMI will ask the server for a copy of the implementation. Should the implementation change tomorrow, a new kind of policy object will be returned to the client, and the RMI runtime will then ask for that new implementation.

This means that policy is always dynamic. You can change the policy by simply writing a new implementation of the general Policy interface, installing it on the server, and configuring the server to return objects of this new type. From that point on, any new expense reports will be checked against the new policy by every client.

This is a better approach than any static approach because:

Here is the remote interface that defines the methods the client can invoke on the server:


import java.rmi.*;
public interface ExpenseServer extends Remote {
    Policy getPolicy() throws RemoteException;
    void submitReport(ExpenseReport report)
        throws RemoteException, InvalidReportException;
}

The import statement imports the Java RMI package. All the RMI types are defined in the package java.rmi or one of its subpackages. The interface ExpenseServer is a normal Java interface with two interesting characteristics

The Policy interface itself declares a method that lets the client know if it is acceptable to add an entry to the expense report:


public interface Policy {
    void checkValid(ExpenseEntry entry)
        throws PolicyViolationException;
}

If the entry is a valid one-one that matches current policy-the method returns normally. Otherwise it throws an exception that describes the error. The Policy interface is local (not remote), and so will be implemented by an object local to the client-one that runs in the client's virtual machine, not across the network. (But below they show a Policy implementation on the server...and Policy is only a method (no variables)? so what exactly is "passed"? ( an explanation) A client would operate something like this:


Policy curPolicy = server.getPolicy();
start a new expense report
show the GUI to the user
while (user keeps adding entries) {
    try {
        curPolicy.checkValid(entry); // throws exception if not OK
        add the entry to the expense report
    } catch (PolicyViolationException e) {
        show the error to the user
    }
}
server.submitReport(report);

When the user asks the client software to start up a new expense report, the client invokes server.getPolicy to ask the server to return an object that embodies the current expense policy. Each entry that is added is first submitted to that policy object for approval. If the policy object reports no error, the entry is added to the report; otherwise the error will be displayed to the user who can take corrective action. When the user is finished adding entries to the report, the entire report is submitted. The server looks like this:


import java.rmi.*;
import java.rmi.server.*;
class ExpenseServerImpl
    extends UnicastRemoteObject
    implements ExpenseServer
{
    ExpenseServerImpl() throws RemoteException {
        // ...set up server state...
    }
    public Policy getPolicy() {
        return new TodaysPolicy();
    }
    public void submitReport(ExpenseReport report) {
        // ...write the report into the db...
    }
}

We import RMI's server package in addition to the basic package. The type UnicastRemoteObject defines the kind of remote object this server will be, in this case a single server as opposed to a replicated service The Java class ExpenseServerImpl implements the methods of the remote interface ExpenseServer. Clients on remote hosts can use RMI to send messages to ExpenseServerImpl objects.

The important method for this discussion is getPolicy, which simply returns an object that defines the current policy. Let's take a look at an example implementation of a policy:


public class TodaysPolicy implements Policy {
    public void checkValid(ExpenseEntry entry)
        throws PolicyViolationException
    {
        if (entry.dollars() < 20) {
            return; // no receipt required
        } else if (entry.haveReceipt() == false) {
            throw new PolicyViolationException;
        }
    }
}

TodaysPolicy checks to ensure that any entry without a receipt is less than $20. If the policy changes tomorrow so that only meals under $20 are exempt from the "receipts required" policy, you could provide a new implementation of policy:


public class TomorrowsPolicy implements Policy {
    public void checkValid(ExpenseEntry entry)
        throws PolicyViolationException
    {
        if (entry.isMeal() && entry.dollars() < 20) {
            return; // no receipt required
        } else if (entry.haveReceipt() == false) {
            throw new PolicyViolationException;
        }
    }
}

Write this class, install it on the server, and tell the server to start handing out TomorrowsPolicy objects instead of TodaysPolicy objects, and your entire system will start using the new policy. When the client invokes the server's getPolicy method, RMI on the client checks to see if the returned object is of a known type. The first time each client encounters a TomorrowsPolicy object, RMI will download the implementation for the policy (?) before getPolicy returns. The client will, without effort, start enforcing the new policy.

RMI uses the standard Java object serialization mechanism to pass objects. Arguments that are references to remote objects are passed as remote references. If an argument to a method is a primitive type or a local (non-remote) object, a deep copy is passed to the server. Return values are handled in the same way, but in the other direction. RMI lets you pass and return full object graphs for local objects and references to remote objects.

In a real system the getPolicy method might have a parameter that identified the user and the kind of expense report (travel, customer relations, etc.) so that the policy can differ. Or instead of requiring separate policy and expense report object, you might have a newExpenseReport method that returned an ExpenseReport object that directly checked the policy. This last strategy would allow you to change the contents of an expense report as easily as the policy-when the company decides that it needs to split out meals into separate breakfast, lunch, and dinner entries that change would be implemented as easily as the new policy shown above-write a new class implementing the report and the client will use it automatically.

See also the Compute Server example. Here the White Paper illustrates how "behavior can flow in both directions-the client can equally pass new types to the user."

RMI Classes

The interfaces and classes that are responsible for specifying the remote behavior of the RMI system are defined in the java.rmi and the java.rmi.server packages. The following figure shows the relationship between these interfaces and classes:

RMI server functions are provided by java.rmi.server.RemoteObject and its subclasses, java.rmi.server.RemoteServer and java.rmi.server.UnicastRemoteObject:

Parameter Passing in Remote Method Invocation

An argument to, or a return value from, a remote object can be any Java type that is serializable. This includes Java primitive types, remote Java objects, and nonremote Java objects that implement the java.io.Serializable interface. For more details on how to make classes serializable, see the Java "Object Serialization Specification." For applets, if the class of an argument or return value is not available locally, it is loaded dynamically via the AppletClassLoader. For applications, these classes are loaded by the class loader that loaded the application; this is either the default class loader (which uses the local class path) or the RMIClassLoader (which uses the server's codebase).

Some classes may disallow their being passed (by not being serializable), for example for security reasons. In this case the remote method invocation will fail with an exception.

Passing Nonremote Objects See also Core Java Vol2, Parameter Passing in Remote Methods section

A nonremote object, that is passed as a parameter of a remote method invocation or returned as a result of a remote method invocation, is passed by copy.

That is, when a nonremote object appears in a remote method invocation, the content of the nonremote object is copied before invoking the call on the remote object. By default, only the nonstatic and nontransient fields are copied.

Similarly, when a nonremote object is returned from a remote method invocation, a new object is created in the calling virtual machine.

Passing Remote Objects See also Core Java Vol2 Passing Remote Objects section

When passing a remote object as a parameter, the stub for the remote object is passed. A remote object passed as a parameter can only implement remote interfaces.

See Core Java Vol2 for an example of inappropriate remote parameters Graphics. Also, for an interesting tidbit you might have missed, see page 259 (in 1.1 edition 1998), the figure.

Locating Remote Objects

A simple bootstrap name server is provided for storing named references to remote objects. A remote object reference can be stored using the URL-based methods of the class java.rmi.Naming.

For a client to invoke a method on a remote object, that client must first obtain a reference to the object. A reference to a remote object is usually obtained as a return value in a method call. The RMI system provides a simple bootstrap name server from which to obtain remote objects on given hosts. The java.rmi.Naming class provides Uniform Resource Locator (URL) based methods to look up, bind, rebind, unbind, and list the name-object pairings maintained on a particular host and port.

Here's an example, (without exception handling) of how to bind and look up remote objects:

BankAccount acct = new BankAcctImpl();
String url = "rmi://java.Sun.COM/account";
// bind url to remote object
java.rmi.Naming.bind(url, acct);
        ...
// lookup account
acct = (BankAccount)java.rmi.Naming.lookup(url);
Also see Core Java Vol 2 Remote Objects Chapter - section "Locating Server Objects"

Dynamic Class Loading See also Core Java Vol2 and the web site ( 1.5)and another web site ( 1.5)

In RPC (remote procedure call) systems, client-side stub code must be generated and linked into a client before a remote procedure call can be done. This code can be either statically linked into the client or linked in at runtime via dynamic linking with libraries available locally or over a network file system. In the case of either static or dynamic linking, the specific code to handle an RPC must be available to the client machine in compiled form.

RMI generalizes this technique, using a mechanism called dynamic class loading to load at runtime (in the Java language's architecture neutral bytecode format) the classes required to handle method invocations on a remote object. These classes are:

This section describes:

In addition to class loaders, dynamic class loading employs two other mechanisms: the object serialization system to transmit classes over the wire, and a security manager to check the classes that are loaded. The object serialization system is discussed in the Object Serialization Specification. Security issues are discussed in a bit.

How a Class Loader is Chosen

In Java, the class loader that initially loads a Java class is subsequently used to load all the interfaces and classes that are used directly in the class:

For objects passed as parameters or return values (the second case above), the URL that is encoded in the stream for an object's class is determined as follows:

Thus, if a class was loaded from CLASSPATH, the codebase URL will be used to annotate that class in the stream if that class is used in an RMI call.

The application can be configured with the property java.rmi.server.useCodebaseOnly, which disables the loading of classes from network hosts and forces classes to be loaded only from the locally defined codebase. If the required class cannot be loaded, the method invocation will fail with an exception.

Bootstrapping the Client

For the RMI runtime to be able to download all the classes and interfaces needed by a client application, a bootstrapping client program is required which forces the use of a class loader (such as RMI's class loader) instead of the default class loader. The bootstrapping program needs to:

For example:

import java.rmi.RMISecurityManager;
import java.rmi.server.RMIClassLoader;

public class LoadClient
{
        public static void main()
        {
                System.setSecurityManager(new RMISecurityManager());
          try {
                        Class cl = RMIClassLoader.loadClass("myclient");
                        Runnable client = (Runnable)cl.newInstance();
                        client.run();
                } catch (Exception e) {
                        System.out.println("Exception: " + e.getMessage());
                        e.printStackTrace();
                }
        }
}
In order for this code to work, you need to specify the java.rmi.server.codebase property when you run the bootstrapping program so that the loadClass method will use this URL to load the class. For example:

java -Djava.rmi.server.codebase=http://host/rmiclasses/ LoadClient
Instead of relying on the property, you can supply your own URL:

Class cl = RMIClassLoader.loadClass(url, "myclient");
Once the client is started and has control, all classes needed by the client will be loaded from the specified URL. This bootstrapping technique is exactly the same technique Java uses to force the AppletClassLoader to download the same classes used in an applet.

Without this bootstrapping technique, all the classes directly referenced in the client code must be available through the local CLASSPATH on the client, and the only Java classes that can be loaded by the RMIClassLoader over the net are classes that are not referred to directly in the client program; these classes are stubs, skeletons, and the extended classes of arguments and return values to remote method invocations.

The RMIClassLoader Class

The java.rmi.server.RMIClassLoader is a utility class that can be used by applications to load classes via a URL.

package java.rmi.server;

public class RMIClassLoader {

    public static Class loadClass(String name)
            throws MalformedURLException, ClassNotFoundException;

    public static synchronized Class loadClass(URL codebase,
                String name) throws MalformedURLException, 
                ClassNotFoundException;

        public static Object getSecurityContext(ClassLoader loader);
}
The first loadClass method loads the specified class name via the URL defined by the java.rmi.server.codebase property. The class is loaded, defined, and returned.

The second form of the loadClass method loads the specified class name via the URL parameter codebase.

The getSecurityContext method returns the security context of the given class loader, loader. The security context is obtained by querying the LoaderHandler's getSecurityContext method.

The RMI runtime uses its own class loader to load stubs, skeletons, and other classes needed by the stubs and skeletons. These classes, and the way they are used, support the safety properties of the Java RMI runtime. This class loader always loads locally-available classes first. Only if a security manager is in force will stubs be loaded from either the local machine or from a network source. The class loader keeps a cache of loaders for individual Uniform Resource Locators (URLs) and the classes that have been loaded from them. When a stub or skeleton has been loaded, any class references that occur as parameters or returns will be loaded (from their originating codebase host) and are subject to the same security restrictions. Server processes must declare to the RMI runtime the location of the classes (stubs and parameters/returns) that will be available to its clients. The java.rmi.server.codebase property should be a URL from which stub classes and classes used by stubs will be loaded, using the normal protocols, such as http and ftp.

Security

In Java, when a class loader loads classes from the local CLASSPATH, those classes are considered trustworthy and are not restricted by a security manager. However, when the RMIClassLoader attempts to load classes from the network, there must be a security manager in place or an exception is thrown.

The security manger must be started as the first action of a Java program so that it can regulate subsequent actions. The security manager ensures that loaded classes adhere to the standard Java safety guarantees, for example that classes are loaded from "trusted" sources (such as the applet host) and do not attempt to access sensitive functions. A complete description of the restrictions imposed by security managers can be found in the documentation for the AppletSecurity class and the RMISecurityManager class.

Applets are always subject to the restrictions imposed by the AppletSecurity class. This security manager ensures that classes are loaded only from the applet host or its designated codebase hosts. This requires that applet developers install the appropriate classes on the applet host.

Applications must either define their own security manager or use the restrictive RMISecurityManager. If no security manager is in place, an application cannot load classes from network sources.

A client or server program is usually implemented by classes loaded from the local system and therefore is not subject to the restrictions of the security manager. If however, the client program itself is downloaded from the network using the technique described in Bootstrapping the Client, then the client program is subject to the restrictions of the security manager.

Once a class is loaded by the RMIClassLoader, any classes used directly by that class are also loaded by the RMIClassLoader and thus are subject to the security manager restrictions.

Even if a security manager is in place, setting the property java.rmi.server.useCodebaseOnly to true prevents the downloading of a class from the URL embedded in the stream with a serialized object (classes can still be loaded from the locally-defined java.rmi.server.codebase). The java.rmi.server.useCodebaseOnly property can be specified on both the client and the server, but is not applicable for applets.

If an application defines its own security manager which disallows the creation of a class loader, classes will be loaded using the default Class.forName mechanism. Thus, a server may define its own policies via the security manager and class loader, and the RMI system will operate within those policies.

The java.lang.SecurityManager abstract class, from which all security managers are extended, does not regulate resource consumption. Therefore, the current RMISecurityManager has no mechanisms available to prevent classes loaded from abusing resources. As new security manager mechanisms are developed, RMI will use them.

Configuration Scenarios

The RMI system supports many different scenarios. Servers can be configured in an open or closed fashion. Applets can use RMI to invoke methods on objects supported on servers. If an applet creates and passes a remote object to the server, the server can use RMI to make a callback to the remote object. Java applications can use RMI either in client-server mode or from peer to peer. This section highlights the issues surrounding these configurations.

Servers

The typical closed-system scenario has the server configured to load no classes. The services it provides are defined by remote interfaces that are all local to the server machine. The server has no security manager and will not load classes even if clients send along the URL. If clients send remote objects for which the server does not have stub classes, those method invocations will fail when the request is unmarshaled, and the client will receive an exception.

The more open server system will define its java.rmi.server.codebase so that classes for the remote objects it exports can be loaded by clients, and so that the server can load classes when needed for remote objects supplied by clients. The server will have both a security manager and RMI class loader which protect the server. A somewhat more cautious server can use the property java.rmi.server.useCodebaseOnly to disable the loading of classes from client-supplied URLs.

Applets

Typically, the classes needed will be supplied by an HTTP server or by an FTP server as referenced in URL's embedded in the HTML page containing the applet. The RMI-based service(s) used by the applet must be on the server from which the applet was downloaded, because an applet can only make network connections to the host from which it was loaded.

For example, the normal applet scenario uses a single host for the HTTP server providing the HTML page, the applet code, the RMI services, and the bootstrap Registry. In this scenario, all the stub, skeleton, and supporting classes are loaded from the HTTP server. All of the remote objects provided by the RMI service and passed to the applet (which may pass them back to the server) will be for classes that the RMI service already knows about. In this case, the RMI service is very secure because it loads no classes from the network.

Applications

Applications written in the Java language, unlike applets, can connect to any host; so Java applications have more options for configuring the sources of classes and where RMI based services run. Typically, a single HTTP server will be used to supply remote classes, while the RMI-based applications themselves are distributed around the network on servers or running on user's desktops

If an application is loaded locally, then the classes used directly in that program must also be available locally. In this scenario, the only classes that can be downloaded from a network source are the classes of remote interfaces, stub classes, and the extended classes of arguments and return values to remote method invocations.

If an application is not loaded from a local directory, but is loaded from a network source using the bootstrapping mechanism then all classes used by the application can be downloaded from the same network source.

To enable downloading from a network source, each remote object server must be configured with the java.rmi.server.codebase property which specifies where application classes and generated stubs/skeletons reside. When the codebase property is specified, the RMI system embeds the URL of a class in the serialized form of the class.

Even if a serialized object's class is annotated with the URL from which the class can be downloaded, a client or peer will still load classes locally if they are available.

RMI Through Firewalls Via Proxies

The RMI transport layer normally attempts to open direct sockets to hosts on the Internet. Many intranets, however, have firewalls which do not allow this. The default RMI transport, therefore, provides two alternate HTTP-based mechanisms which enable a client behind a firewall to invoke a method on a remote object which resides outside the firewall.

How an RMI Call is Packaged within the HTTP Protocol

To get outside a firewall, the transport layer embeds an RMI call within the firewall-trusted HTTP protocol. The RMI call data is sent outside as the body of an HTTP POST request, and the return information is sent back in the body of the HTTP response. The transport layer will formulate the POST request in one of two ways:

  1. If the firewall proxy will forward an HTTP request directed to an arbitrary port on the host machine, then it is forwarded directly to the port on which the RMI server is listening. The default RMI transport layer on the target machine is listening with a server socket that is capable of understanding and decoding RMI calls inside POST requests.
  2. If the firewall proxy will only forward HTTP requests directed to certain well-known HTTP ports, then the call will be forwarded to the HTTP server listening on port 80 of the host machine, and a CGI script will be executed to forward the call to the target RMI server port on the same machine.

The Default Socket Factory

The RMI transport extends the java.rmi.server.RMISocketFactory class to provide a default implementation of a socket factory which is the resource- provider for client and server sockets. This default socket factory creates sockets that transparently provide the firewall tunnelling mechanism as follows:

Client-side sockets, with this default behavior, are provided by the factory's java.rmi.server.RMISocketFactory.createSocket method. Server- side sockets with this default behavior are provided by the factory's java.rmi.server.RMISocketFactory.createServerSocket method.

Configuring the Client

There is no special configuration necessary to enable the client to send RMI calls through a firewall.

The client can, however, disable the packaging of RMI calls as HTTP requests by setting the java.rmi.server.disableHttp property to equal the boolean value true.

Configuring the Server

The host name should not be specified as the host's IP address, because some firewall proxies will not forward to such a host name.

  1. In order for a client outside the server host's domain to be able to invoke methods on a server's remote objects, the client must be able to find the server. To do this, the remote references that the server exports must contain the fully-qualified name of the server host. Depending on the server's platform and network environment, this information may or may not be available to the Java virtual machine on which the server is running. If it is not available, the host's fully qualified name must be specified with the property java.rmi.server.hostname when starting the server.

    For example, use this command to start the RMI server class ServerImpl on the machine chatsubo.javasoft.com:

       java -Djava.rmi.server.hostname=chatsubo.javasoft.com ServerImpl
    
  2. If the server will not support RMI clients behind firewalls that can forward to arbitrary ports, use this configuration:
    1. An HTTP server is listening on port 80.
    2. A CGI script is located at the aliased URL path /cgi-bin/java-rmi. This script:

Performance Issues and Limitations

Calls transmitted via HTTP requests are at least an order of magnitude slower that those sent through direct sockets, without taking proxy forwarding delays into consideration.

Because HTTP requests can only be initiated in one direction through a firewall, a client cannot export its own remote objects outside the firewall, because a host outside the firewall cannot initiate a method invocation back on the client.

RMI's Use of HTTP POST Protocol

In order to invoke remote methods through a firewall, some RMI calls make use of the HTTP protocol, more specifically HTTP POST. The URL specified in the post header can be one of the following:

  
http://<host>:<port>/ 
http://<host>:80/cgi-bin/java-rmi?forward=<port>

The first URL is used for direct communication with an RMI server on the specific host and port. The second URL form is used to invoke a "cgi" script on the server which forwards the invocation to the server on the specified port.

An HttpPostHeader is a standard HTTP header for a POST request. An HttpResponseHeader is a standard HTTP response to a post. If the response status code is not 200, then it is assumed that there is no Return. Note that only a single RMI call is embedded in an HTTP POST request.

HttpMessage:
        HttpPostHeader Header Message
HttpReturn:
        HttpResponseHeader Return
Only the SingleOpProtocol appears in the Header of an HttpMessage. An HttpReturn does not contain a protocol acknowledgment byte.

Why would I want to use RMI?

Once we know how to get it going, for some more "useful" examples: