22.4 A Servlet Implementationof HTTP Tunneling
For the fourth and fifth connection strategies to work, there has to be a way, hooked into a web server, to forward method invocations to the actual RMI server. The first implementation of this
that Sun Microsystems, Inc. shipped was a compiled CGI script. Formally, it required the following:
• Each remote method invocation was sent as a HTTP POST request.
• The full URL being used was of the form cgi-binjava-rmi.cgi?forward=[port number].
• The body of the POST contained all the data from the remote request as serialized
objects that were then converted into an ASCII stream of characters. After the servlet specification was defined, Sun shipped a servlet that provided the same
functionality as the CGI script. In the discussion that follows, we will use a stripped-down version of the
SUN class to demonstrate how HTTP tunneling works.
In the remainder of this section, we will examine the servlet code in detail.
22.4.1 The Servlet Code
The stripped-down servlet code consists of two main classes and a set of custom exception classes. The main classes are
SimplifiedServletHandler and
ServletForwardCommand .
They have the following roles: SimplifiedServletHandler
This class extends HTTPServlet
. It receives requests and performs preliminary validation most of which centers around processing the URL.
ServletForwardCommand This is a set of static methods that know how to take an HTTP POST and resend it to an
RMI server running on the same machine. Heres the code for
SimplifiedServletHandler :
public class SimplifiedServletHandler extends HttpServlet { public void initServletConfig config throws ServletException {
super.initconfig;
System.out.printlnSimplified RMI Servlet Handler loaded successfully.;
} public void doPostHttpServletRequest req, HttpServletResponse
res throws ServletException, IOException {
try { String queryString = req.getQueryString ;
String command, param; int delim = queryString.indexOf=;
if delim == -1 { command = queryString;
param = ; }
else { command = queryString.substring0,
delim; param = queryString.substringdelim + 1;
} if command.equalsIgnoreCaseforward {
try { ServletForwardCommand.executereq, res, pa ram;
} catch ServletClientException e
{ returnClientErrorres, client
error: + e.getMessage ; e.printStackTrace ;
} catch ServletServerException e {
returnServerErrorres, internal server error: + e.
getMessage ; e.printStackTrace ;
} }
else { returnClientErrorres, invalid command:
+ command; }
} catch Exception e {
returnServerErrorres, internal error: + e.getMessage ;
e.printStackTrace ; }
} public void doGetHttpServletRequest req, HttpServletResponse
res throws ServletException, IOException {
returnClientErrorres, GET Operation not supported: + Can only forward POST requests.;
}
public void doPutHttpServletRequest req, HttpServletResponse res throws
ServletException, IOException { returnClientErrorres, PUT Operation not supported: +
Can only forward POST requests.; }
public String getServletInfo { return Simplified RMI Call Forwarding Servlet
Servlet.br\n; }
private static void returnClientErrorHttpServletResponse res, String message
throws IOException { res.sendErrorHttpServletResponse.SC_BAD_REQUEST,
HTMLHEADTITLEJava RMI Client ErrorTITLEHEADBODY +
H1Java RMI Client ErrorH1 + message + BODYHTML;
System.err.printlnHttpServletResponse.SC_BAD_REQUEST + Java RMI Client Error + message;
} private static void returnServerErrorHttpServletResponse res,
String message throws IOException {
res.sendErrorHttpServletResponse.SC_INTERNAL_SERVER_ERROR, HTMLHEAD
TITLEJava RMI Server ErrorTITLEHEADBODY + H1Java RMI
Server ErrorH1 + message + BODYHTML;
System.err.printlnHttpServletResponse.SC_INTERNAL_SERVER_ERROR + Java RMI
Server Error: + message; }
} Almost all of the processing logic for
SimplifiedServletHandler is contained inside the
doPost method. The
doPost method parses the URL to determine the port on which
the server listens. If the URL is a valid URL and contains a port, the request is forwarded to the ServletForwardCommand
s static execute
method. ServletForwardCommand
consists of the following static methods: public static void executeHttpServletRequest request,
HttpServletResponse response, String stringifiedPort throws ServletClientException,
ServletServerException, IOException
{ int port = convertStringToPortstringifiedPort;
Socket connectionToLocalServer = null; try {
connectionToLocalServer = connectToLocalServerport;
forwardRequestrequest, connectionToLocalServer; forwardResponseresponse,
connectionToLocalServer; }
finally { if null=connectionToLocalServer {
connectionToLocalServer.close ; }
} }
private static int convertStringToPortString stringfiedPort throws
ServletClientException { int returnValue;
try { returnValue = Integer.parseIntstringfiedPort;
} catch NumberFormatException e {
throw new ServletClientExceptioninvalid port number: +
stringfiedPort; }
if returnValue = 0 || returnValue 0xFFFF { throw new ServletClientExceptioninvalid port:
+ returnValue; }
if returnValue 1024 { throw new ServletClientExceptionpermission
denied for port: + returnValue;
} return returnValue;
} private static Socket connectToLocalSe rverint port throws
ServletServerException { Socket returnValue;
try { returnValue = new
SocketInetAddress.getLocalHost , port; }
catch IOException e { throw new ServletServerExceptioncould not
connect to + local port;
} return returnValue;
} private static void forwardResponseHttpServletResponse
response, Socket connectionToLocalServer throws IOException,
ServletClientException, ServletServerException
{ byte[] buffer;
DataInputStream socketIn; try {
socketIn = new DataInputStreamconnectionToLocalServer.getInputStream ;
} catch IOException e {
throw new ServletServerExceptionerror reading from + server;
} String key = Content-length:.toLowerCase ;
boolean contentLengthFound = false; String line;
int responseContentLength = -1; do {
try { line = socketIn.readLine ;
} catch IOException e {
throw new ServletServerExceptionerror reading from server;
} if line == null {
throw new ServletServerException unexpected EOF reading server response;
} if line.toLowerCase .startsWithkey {
responseContentLength = Integer.parseIntline.substringkey.
length.trim ; contentLengthFound = true;
} }
while line.length = 0 line.charAt0 = \r line.charAt0
= \n; if contentLengthFound || responseContentLeng th 0
throw new ServletServerExceptionmissing or invalid content length
in server response;
buffer = new byte[responseContentLength]; try {
socketIn.readFullybuffer; }
catch EOFException e { throw new ServletServerExceptionunexpected EOF
reading server response;
} catch IOException e {
throw new ServletServerExceptionerror reading from server;
} response.setStatusHttpServletResponse.SC_OK;
response.setContentTypeapplicationoctet-stream; response.setContentLengthbuffer.length;
try { OutputStream out = response.getOutputStream ;
out.writebuffer; out.flush ;
} catch IOException e {
throw new ServletServerExceptionerror writi ng response;
} }
} There are three important points to notice about
ServletForwarder . First, it uses sockets and
streams to forward the body of the HTTP message to the RMI server and then simply pipes what the RMI server returns back into
HttpServletResponse s output stream. There is no magical
or mysterious code here, and HTTP tunneling involves nothing that you hadnt seen by the end of Chapt er 2
. The second important point is that
ServletForwarder forwards the message without
demarshalling the data or understanding the message at all. Even though the body of the HTTP request contains serialized instances of classes,
ServletForwarder doesnt need to have the
classes on its classpath or treat the serialized instances as anything other than bytes to be copied from one stream to another. This means that HTTP tunneling is easy to deploy and doesnt need
to be updated as the application evolves.
The final point is that, all tolled, this is really a simple piece of code that takes advantage of a lot of existing infrastructure. You can download the Apache web server available at
ht t p: w w w .apache.or g , install the Tomcat servlet engine available at
ht t p: j akar t a.apache.or g , and, in the course of an afternoon, circumvent a firewall that took
experienced systems administrators months to configure and install.
22.5 Modifying the Tunneling Mechanism