APACHE - Tomcat - RMI HTTP TUNNELLING HOW-TO

Or

10 Steps to RMI Tunneling

David M. Howard
www.dmh2000.com
davidh@sncorp.com

Background

Remote Method Invocation (RMI) in Java gives a clean way to build distributed applications in java. If you are on an Intranet with no firewalls, RMI just works. But on the Internet, where firewalls abound, RMI can be blocked because it tries to connect to server hosts using arbitrary port numbers. Typical firewalls on the server side don't allow this. To alleviate this problem, Sun built in to the RMI implementation a fall back to HTTP tunnelling to provide a way for RMI to get through firewalls. See the RMI FAQ for details. The best short tutorial I have found is is at http://developer.java.sun.com/developer/onlineTraining/rmi/RMI.html. Read this first if you have any questions about how RMI HTTP Tunnelling works.

The bottom line is that if a direct connection is blocked by a firewall on the server side, the RMI client side will create an HTTP request of the form http://hostname:80/cgi-bin/java-rmi.cgi?port=n and try to get to the server that way. The catch is there has to be a web server running at hostname:80 on the same machine that is running the RMI server task. The web server needs to know how to forward RMI requests to the local RMI server.

Sun provides two implementations of programs that will forward the request properly. The default method is a CGI script called java-rmi.cgi which you can just drop in to the cgi-bin directory of Apache and it will work. This script is included in the Unix/Linux JDK distributions. But this script has the performance limitation of all CGI scripts plus it has to spawn a java VM to do the work. So it is noticeably slow. Sun also provides an example servlet implementation that has to run in a servlet container. The servlet implementation needs a servlet container to run in. Once set up, the servlet implementation is substantially faster than the cgi script version.

Unfortunately, the Sun servlet implementation comes with a readme.txt file that describes how to get it working in the old Java Web Server environment and it has nothing about getting it to work in Apache+Tomcat other than the description of the general approach. The setup for the old JWS is way different from Apache +Tomcat.

This how-to attempts to give a step by step approach to getting the Sun RMI servlet handler to work in Apache+Tomcat. There are a lot of network architecture, security and performance considerations that come into play when using RMI on the internet. I don't answer those questions here. I just describe how to get a particular setup to work. In particular, this HOW-TO addresses setup on the SERVER side only. The above references tell you what a client will need to do to get through a client-side firewall.

Software Versions

Here are the steps I followed to get RMI Tunneling to work:

1. INSTALL THE JDK

make sure the jdk/bin directory is in your path make sure you can build and run java programs per the JDK installation instructions.

You will also need to modify the 'java.policy' security profile on the server side to allow the server to access the socket ports it needs. Add the following lines to your java.policy file, which is located in the jre/lib/security directory of the Java runtime.

	// allow server to listen on un-privileged ports
	permission java.net.SocketPermission "localhost:1024-", "listen,connect,resolve";

2. INSTALL APACHE AND GET IT TO RUN

Follow the normal Apache installation instructions. Verify that you can see the home page

3. MAKE SURE CGI SCRIPTS ARE EXECUTABLE

Make sure Apache is set up to run cgi-scripts.

Point a (preferably remote) browser at http://hostname/cgi-bin/test-cgi you should get a printout of the script environment, or you will get server errors. If it works, great. If not, here are some tips:

4. GET THE java-rmi.cgi SCRIPT WORKING

5. TEST THE SampleRMI CLIENT AND SERVER

6. INSTALL TOMCAT

Install Tomcat and get it to the point that example servlets work. Also get it integrated into Apache so that static HTML is served by Apache. That is all in the Tomcat installation guide so I won't repeat it here. See http://jakarta.Apache.org/Tomcat/Tomcat-3.2-doc/Tomcat-Apache-howto.html for more info.

Point your browser at http://hostname/examples/servlets/ and it should let you exercise the Tomcat sample servlets. If you get errors fix them according to the Tomcat documentation.

7. INSTALL A WEB APPLICATION WITH THE ServletHandler SERVLET

8. MAP APACHE TO POINT TO THE SERVLET

Next, we want to get rid of the port ':8080' so we have to set up the Tomcat mod_jk.conf file to map the rmi web application thru Apache. If you are using mod_jk.conf-auto the mapping will be there automatically. Otherwise you must edit <tomcat>/conf/mod_jk.conf and add the following:

   #########################################################
   # Auto configuration for the /rmi context starts.
   #########################################################
   #
   # The following line makes Apache aware of the location of the /rmi context
   #
   Alias /rmi "/usr/local/Apache/jakarta-Tomcat-3.2.3/webapps/rmi"
   <Directory "/usr/local/Apache/jakarta-Tomcat-3.2.3/webapps/rmi">
   Options Indexes FollowSymLinks
   </Directory>
   #
   # The following line mounts all JSP files and the /servlet/ uri to Tomcat
   #
   JkMount /rmi/servlet/* ajp12
   JkMount /rmi/*.jsp ajp12
   #
   # The following line prohibits users from directly accessing WEB-INF
   #
   <Location "/rmi/WEB-INF/">
   AllowOverride None
   deny from all
   </Location>
   #
   # The following line prohibits users from directly accessing META-INF
   #
   <Location "/rmi/META-INF/">
   AllowOverride None
   deny from all
   </Location>
   #######################################################
   # Auto configuration for the /rmi context ends.
   ####################################################### 
Restart Apache and Tomcat. Now you should be able to access the servlet as    before but without the port specification.

9. REDIRECT THE CGI SCRIPT

We are almost there. One more thing.

The Java RMI classes are hard coded to expect an executable with the name /cgi-bin/java-rmi.cgi to be the target of HTTP tunnelling. To my knowledge there is no way to change this. So the next step is to set up an Apache alias to force an INTERNAL redirect from /cgi-bin/java-rmi.cgi to the servlet. An external redirect (using the Apache configuration option 'Redirect') will cause an access exception on the client side. The client will detect there was a redirect and will throw an exception. You can fix this exception by enabling the proper security permissions on the client side but that is usually an undesirable solution.

I'm not an Apache expert, so I had to dig a little to figure out to make the redirect work. Two Apache modules used for redirecting are mod_alias and mod_rewrite. Alias doesn't seem to work because the actual servlet is at a virtual address controlled by Tomcat, so you can't just substitute a new file path for Apache to fetch. Instead you need an alias that forces the URL to be sent to Tomcat. So I ended up using URL rewriting with mod_rewrite to do an internal proxy redirect.

10. RERUN THE SAMPLE PROGRAM


NOTES FROM JDK 1.4 and Tomcat 3.3a

If you get an exception on the client side saying 'can't connect to 127.0.0.1:80' (or something like that).
Since you aren't trying to connect to the web server on the client side, you wonder why it is doing this. This won't happen is you test the client and server on the same machine. This happens when the server thinks that its "public" interface is 127.0.0.1 and not the server's real address. To fix this, the server needs to know what IP address or host name it should tell the client to use for subsequent requests. This is done by specifying the "java.rmi.server.hostname" system property, by adding the following to the command line used to start the server:

start the sample server (an your server when the time comes) by hand with the option -Djava.rmi.server.hostname=<real ip address>

java -Djava.rmi.server.hostname=<...>

Java SocketPermission exceptions
if any security exceptions occur listing a socket:port and 'connect,resolve,accept' etc, edit the proper
java.policy file to grant the appropriate permissions. you will probably need to open up
ip's and ports on both sides, client and server because the default java.policy file has some
strict permissions.

// here is what I ended up with on the client side
   permission java.net.SocketPermission "localhost:1024-", "listen,connect,resolve";
   permission java.net.SocketPermission "127.0.0.1:1024-", "listen,connect,resolve";
   permission java.net.SocketPermission "10.6.128.234:80-", "connect,resolve";
// here is what i ended up with on the server side
   permission java.net.SocketPermission "localhost:80-", "listen,connect,resolve";
   permission java.net.SocketPermission "127.0.0.1:80-", "listen,connect,resolve";
   permission java.net.SocketPermission "*:1024-", "listen,accept,connect,resolve";

There may be some security implications with these settings that I don't know about. I just know they worked for this particular problem.

Tomcat 3.3a mod_jk.conf is simpler.

you just need to add :

   JkMount /rmi ajp13
   JkMount /rmi/* ajp13

the other stuff appears not be necessary

On tomcat 3.3a I couldn't get the SampleRMIServer to start automatically.
Instead I had to start it manually from the linux command line. I didn't have time to investigate the problems. It may be something simple.

Classpath issues with JDK 1.4 for Linux
I found that the RMI servers wouldn't work properly when I started them with a '-cp' spec for the classpath. I would get ClassNotFoundExceptions for the RMI stubs. But it worked OK if I set the classpath at the command line with 'export CLASSPATH=..." and then just ran the server without a -cp spec.
Again, something simple probably or a 1.4 implementation quirk.


Any feedback on this HOW-TO is appreciated. I have gone thru the process twice and it worked both times. However there are always gotcha's for particulat implementations so let me know what happens.

1