org.acplt.oncrpc.OncRpcHttpClient Maven / Gradle / Ivy
Show all versions of remotetea-oncrpc Show documentation
/*
* $Header: /home/harald/repos/remotetea.sf.net/remotetea/src/org/acplt/oncrpc/OncRpcHttpClient.java,v 1.5 2005/11/11 21:18:43 haraldalbrecht Exp $
*
* Copyright (c) 1999, 2000
* Lehrstuhl fuer Prozessleittechnik (PLT), RWTH Aachen
* D-52064 Aachen, Germany.
* All rights reserved.
*
* This library is free software; you can redistribute it and/or modify
* it under the terms of the GNU Library General Public License as
* published by the Free Software Foundation; either version 2 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 Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this program (see the file LICENSE.txt for more
* details); if not, write to the Free Software Foundation, Inc.,
* 675 Mass Ave, Cambridge, MA 02139, USA.
*/
package org.acplt.oncrpc;
import java.io.IOException;
import java.io.InterruptedIOException;
import org.acplt.oncrpc.web.*;
/**
* ONC/RPC client which communicates with ONC/RPC servers over the network
* using the ISO/OSI level 7 application protocol HTTP as a tunnel.
*
* Please note that currently no standard exists about how to tunnel
* XDR data over HTTP connections. There are a few solutions out there, but
* they are more or less incompatible due to the lack of an RFC. So I'm now
* adding yet another proprietary solution.
*
*
The protocol which is used here is rather simple and tries to be
* compatible with as much firewall systems as possible. For this to achieve,
* both the ONC/RPC calls as well as their replies are first base64 encoded,
* before they are sent through the tunnel. This way, calls and replies appear
* to be ordinary text documents of mime type "text/plain".
*
*
Calls will appear to be something like this, carrying redirection
* information. Note that we do not include ONC/RPC call information in the
* header section, as this is already included in the ONC/RPC call itself.
* So including it in the header would just make it easier to play games
* with the header.
*
* CALL host name:port protocol TEA/1<CR><LF>
* B0D0EADSDEADBEEF...<CR><LF>
* ...<CR><LF>
* DEADBE==<CR><LF>
*
*
* Replies do not carry the redirection head, but only the base64 encoded
* data of the ONC/RPC reply:
*
* B0D0EADSDEADBEEF...<CR><LF>
* ...<CR><LF>
* DEADBE==<CR><LF>
*
*
* The decoding from the base64 encoded data is carried out by the
* {@link XdrHttpDecodingStream} class.
*
*
I'm not using eecks-emm-ell on purpose (net yet). While it is surely
* fun to play with and it has its merits, just to create yet another RPC
* tunnel parsing XML is still too large an overhead to accept. Despite cynics
* pointing out that the Internet is already that slow so that XML overhead
* will not be visible at all, I nevertheless disagree. On the other hand,
* XML would be really fine to be misused for yet another proprietary and
* misguided ASCII, pardon UTF-8, data format...
*
* <?xml version="1.0"?>
* <!DOCTYPE oncrpc-call SYSTEM "oncrpc-call.dtd">
* <oncrpc-call server="foo.bar.com" protocol="tcp">
* B0D0EADSDEADBEEF...<CR><LF>
* ...<CR><LF>
* DEADBE==<CR><LF>
* </oncrpc-call>
*
*
* The answer then could be represented as follows:
*
* <?xml version="1.0"?>
* <!DOCTYPE oncrpc-reply SYSTEM "oncrpc-reply.dtd">
* <oncrpc-reply>
* B0D0EADSDEADBEEF...<CR><LF>
* ...<CR><LF>
* DEADBE==<CR><LF>
* </oncrpc-reply>
*
*
* So it should be fairly easy to switch over to XML if someone will
* insist on it. Reminds me of my Xmas lecture about "Internet Technologies --
* Sacred Land of the Automation Industry?"...
*
* @version $Revision: 1.5 $ $Date: 2005/11/11 21:18:43 $ $State: Exp $ $Locker: $
* @author Harald Albrecht
*
* @see XdrHttpDecodingStream
* @see org.acplt.oncrpc.web.HttpClientConnection
*/
public class OncRpcHttpClient extends OncRpcClient {
/**
* Constructs a new OncRpcHttpClient
object, which connects
* to the ONC/RPC server at host
for calling remote procedures
* of the given { program, version }. At the other end of the HTTP tunnel,
* TCP/IP is used to call the ONC/RPC server.
*
*
Note that the HTTP connection is not build before the first ONC/RPC
* call is done through the {@link #call} method. The HTTP client tries to
* keep the connection alive but reconnects if necessary. Nevertheless, as
* it signals all failures, the caller has to handle reconnect situations
* -- but this is easy to achieve.
*
* @param hostname The DNS name of the host where the ONC/RPC server
* resides.
* @param cgiHandlerPath The path to the CGI Handler which will redirect
* ONC/RPC calls to the particular ONC/RPC servers.
* @param oncrpcHostname The DNS name of the ONC/RPC server to contact.
* @param program Program number of the ONC/RPC server to call.
* @param version Program version number.
* @param port The port number where the ONC/RPC server can be contacted.
* If 0
, then the other end of the HTTP tunnel will try to
* ask the portmapper at host
for the port number.
*
* @throws OncRpcException if an ONC/RPC error occurs.
* @throws IOException if an I/O error occurs.
*/
public OncRpcHttpClient(String hostname,
String cgiHandlerPath,
String oncrpcHostname,
int program, int version, int port)
throws OncRpcException, IOException {
this(hostname,
HttpClientConnection.HTTP_DEFAULTPORT,
cgiHandlerPath,
oncrpcHostname,
program, version, port,
OncRpcProtocols.ONCRPC_TCP);
}
/**
* Constructs a new OncRpcHttpClient
object, which connects
* to the ONC/RPC server at host
for calling remote procedures
* of the given { program, version }.
*
*
Note that the HTTP connection is not build before the first ONC/RPC
* call is done through the {@link #call} method. The HTTP client tries to
* keep the connection alive but reconnects if necessary. Nevertheless, as
* it signals all failures, the caller has to handle reconnect situations
* -- but this is easy to achieve.
*
* @param hostname The DNS name of the host where the ONC/RPC server
* resides.
* @param cgiHandlerPath The path to the CGI Handler which will redirect
* ONC/RPC calls to the particular ONC/RPC servers.
* @param oncrpcHostname The DNS name of the ONC/RPC server to contact.
* @param program Program number of the ONC/RPC server to call.
* @param version Program version number.
* @param port The port number where the ONC/RPC server can be contacted.
* If 0
, then the other end of the HTTP tunnel will try to
* ask the portmapper at host
for the port number.
* @param protocol Transport protocol to be used by the other end of
* the tunnel to call the ONC/RPC server.
*
* @throws OncRpcException if an ONC/RPC error occurs.
* @throws IOException if an I/O error occurs.
*/
public OncRpcHttpClient(String hostname,
String cgiHandlerPath,
String oncrpcHostname,
int program, int version, int port,
int protocol)
throws OncRpcException, IOException {
this(hostname,
HttpClientConnection.HTTP_DEFAULTPORT,
cgiHandlerPath,
oncrpcHostname,
program, version, port,
protocol);
}
/**
* Constructs a new OncRpcHttpClient
object, which connects
* to the ONC/RPC server at host
for calling remote procedures
* of the given { program, version }.
*
*
Note that the HTTP connection is not build before the first ONC/RPC
* call is done through the {@link #call} method. The HTTP client tries to
* keep the connection alive but reconnects if necessary. Nevertheless, as
* it signals all failures, the caller has to handle reconnect situations
* -- but this is easy to achieve.
*
* @param hostname The DNS name of the host where the ONC/RPC server
* resides.
* @param httpPort The port number where the HTTP server is to be
* contacted.
* @param cgiHandlerPath The path to the CGI Handler which will redirect
* ONC/RPC calls to the particular ONC/RPC servers.
* @param oncrpcHostname The DNS name of the ONC/RPC server to contact.
* @param program Program number of the ONC/RPC server to call.
* @param version Program version number.
* @param port The port number where the ONC/RPC server can be contacted.
* If 0
, then the other end of the HTTP tunnel will try to
* ask the portmapper at host
for the port number.
* @param protocol Transport protocol to be used by the other end of
* the tunnel to call the ONC/RPC server.
*
* @throws OncRpcException if an ONC/RPC error occurs.
* @throws IOException if an I/O error occurs.
*/
public OncRpcHttpClient(String hostname,
int httpPort,
String cgiHandlerPath,
String oncrpcHostname,
int program, int version, int port,
int protocol)
throws OncRpcException, IOException {
//
// Construct the inherited part of our object. This will also try to
// lookup the port of the desired ONC/RPC server, if no port number
// was specified (port = 0).
//
// PLEASE NOTE:
// - We supply a null InetAddress to our parent class, as we do not
// make use of an InetAddress at all but rather juggle with the
// plain host name. This is necessary in order to support virtual
// hosting of web servers, and the HTTP client object needs to
// know to which particular web server it contacts, despite of
// the server's internet address.
// - The parent class will not try to look up the port address
// as we signal that we are going to use HTTP tunneling.
//
super(null, program, version, -1, OncRpcProtocols.ONCRPC_HTTP);
this.httpPort = httpPort;
this.cgiHandlerPath = cgiHandlerPath;
this.oncrpcHostname = oncrpcHostname;
this.oncrpcProtocol = protocol;
this.port = port;
//
// We need to work with the host name instead of its address
// so we can support virtual web hosting, where multiple DNS names
// map to the same IP address, and the client sends the host name
// it wants to contact within the HTTP requests.
//
this.hostname = hostname;
httpClient = new HttpClientConnection(hostname, httpPort);
//
// Create the necessary encoding and decoding streams, so we can
// communicate at all.
// FIXME: write specialized dynamically buffered encoding stream!
sendingXdr = new XdrBufferEncodingStream(8192);
receivingXdr = new XdrHttpDecodingStream(httpClient);
}
/**
* Close the connection to an ONC/RPC server and free all network-related
* resources. Well -- at least hope, that the Java VM will sometimes free
* some resources. Sigh.
*
* @throws OncRpcException if an ONC/RPC error occurs.
*/
public void close()
throws OncRpcException {
if ( httpClient != null ) {
httpClient.close();
httpClient = null;
}
//
// Close both XDR streams individually to avoid missing the second
// one when the first one throws an exception.
//
if ( sendingXdr != null ) {
try {
sendingXdr.close();
} catch ( IOException e ) {
} finally {
sendingXdr = null;
}
}
if ( receivingXdr != null ) {
try {
receivingXdr.close();
} catch ( IOException e ) {
} finally {
receivingXdr = null;
}
}
}
/**
* Calls a remote procedure on an ONC/RPC server.
*
*
FIXME: timeout control?
*
* @param procedureNumber Procedure number of the procedure to call.
* @param versionNumber Protocol version number.
* @param params The parameters of the procedure to call, contained
* in an object which implements the {@link XdrAble} interface.
* @param result The object receiving the result of the procedure call.
*
* @throws OncRpcException if an ONC/RPC error occurs.
*/
public synchronized void call(int procedureNumber, int versionNumber,
XdrAble params, XdrAble result)
throws OncRpcException {
int responseCode;
Refresh:
for ( int refreshesLeft = 1; refreshesLeft >= 0;
--refreshesLeft ) {
//
// First, build the ONC/RPC call header. Then put the sending
// stream into a known state and encode the parameters to be
// sent. Finally tell the encoding stream to send all its data
// to the server. Then wait for an answer, receive it and decode
// it. So that's the bottom line of what we do right here.
//
nextXid();
OncRpcClientCallMessage callHeader =
new OncRpcClientCallMessage(xid,
program, versionNumber, procedureNumber,
auth);
OncRpcClientReplyMessage replyHeader =
new OncRpcClientReplyMessage(auth);
//
// Send call message to server. If we receive an IOException,
// then we'll throw the appropriate ONC/RPC (client) exception.
// Note that we use a connected stream, so we don't need to
// specify a destination when beginning serialization.
//
try {
sendingXdr.beginEncoding(null, 0);
callHeader.xdrEncode(sendingXdr);
params.xdrEncode(sendingXdr);
sendingXdr.endEncoding();
} catch ( IOException e ) {
throw(new OncRpcException(OncRpcException.RPC_CANTSEND,
e.getLocalizedMessage()));
}
//
// Embedd the ONC/RPC call request within an unsuspiciously
// looking ASCII page. Well, while I don't like this, it is
// a good way to get access to a remote system from behind
// a company's firewall, when the sysadmin disables free access
// to that "domain of darkness" also known as "The Internet".
//
StringBuffer prefix = new StringBuffer(512);
//
// For calls we need to add some "bang information" (courtesty of
// UU), so the other end of the HTTP tunnel can redirect the
// ONC/RPC calls to the proper ONC/RPC server. Note that we
// send only the bare minimum for routing. The missing information
// is already contained in the ONC/RPC header following, so we
// do not duplicate it. I want to avoid making it too easy to
// spoof redirection information.
//
prefix.append("CALL "); // tunnel method
prefix.append(oncrpcHostname); // host to contact
if ( port > 0 ) { // optional port number
prefix.append(":");
prefix.append(port);
}
prefix.append(" ");
prefix.append(oncrpcProtocol); // transport protocol
prefix.append(" ");
prefix.append(HttpTunnelConstants.TUNNEL_PROTO_ID); prefix.append("\r\n");
//
// Terminate header section.
//
prefix.append("\r\n");
//
// Ccalculate the length of the full content, including the
// ASCII-ized ONC/RPC call record.
//
int contentLength = sendingXdr.getXdrLength();
int lineCount = (contentLength + (HttpTunnelConstants.BYTES_PER_LINE - 1))
/ HttpTunnelConstants.BYTES_PER_LINE;
int realLength = prefix.length()
+ ((contentLength + 2) / 3) * 4 + lineCount * 2
;
// FIXME
byte [] xdrData = sendingXdr.getXdrData();
//
// We are now ready to start the POST request, which will carry
// our ONC/RPC call record to the HTTP server and beyond it to
// the destination ONC/RPC server.
//
try {
httpClient.beginPostRequest(cgiHandlerPath,
"text/plain", realLength);
//
// First, send the HTTP tunnel ONC/RPC header. This header
// contains "routing information", so that the receiver at
// the other end of the HTTP tunnel can redirect the call
// to the appropriate ONC/RPC server.
//
String s = prefix.toString();
httpClient.writeContentBytes(s.getBytes(), 0, s.length());
//
// Next, encode the binary XDR data. This is done in blocks,
// speeding up things a little bit by avoiding making too
// many single calls into the output stream (although buffered).
//
byte [] lines = new byte[HttpTunnelConstants.ENCODED_BYTES_PER_LINE_CRLF
* HttpTunnelConstants.LINES_PER_BLOCK];
int xdrOffset = 0;
int offset = 0;
while ( contentLength >= HttpTunnelConstants.BYTES_PER_LINE ) {
if ( offset >= (HttpTunnelConstants.ENCODED_BYTES_PER_LINE_CRLF
* HttpTunnelConstants.LINES_PER_BLOCK) ) {
httpClient.writeContentBytes(lines, 0, offset);
offset = 0;
}
offset += Base64.encode(xdrData, xdrOffset,
HttpTunnelConstants.BYTES_PER_LINE,
lines, offset);
lines[offset++] = 13;
lines[offset++] = 10;
xdrOffset += HttpTunnelConstants.BYTES_PER_LINE;
contentLength -= HttpTunnelConstants.BYTES_PER_LINE;
}
//
// If there's still data left over which did not fill a
// complete line then generate the last line and flush it.
//
if ( contentLength > 0 ) {
if ( offset >= (HttpTunnelConstants.ENCODED_BYTES_PER_LINE_CRLF
* HttpTunnelConstants.LINES_PER_BLOCK) ) {
httpClient.writeContentBytes(lines, 0, offset);
offset = 0;
}
offset += Base64.encode(xdrData, xdrOffset, contentLength,
lines, offset);
lines[offset++] = 13;
lines[offset++] = 10;
}
if ( offset > 0 ) {
httpClient.writeContentBytes(lines, 0, offset);
}
//
// Indicate the end of the request, so that all data gets sent
// to the HTTP server for processing.
//
httpClient.endPostRequest();
} catch ( IOException e ) {
throw(new OncRpcException(OncRpcException.RPC_CANTSEND,
e.getLocalizedMessage()));
}
//
// Receive reply message from server -- at least try to do so...
// IMPORTANT NOTE:
// - we do not support batched calls through HTTP tunnels, as then
// an additional record layer would have been to be put between
// the HTTP post and the base64 encoding layer.
//
try {
//
// In contrast to TCP/IP and UDP/IP-based transports, we
// can expect the tunnel to be responding with the matching
// ONC/RPC reply. If it does not, there's no way of waiting
// for another reply -- because we only have the strict
// "one call, one reply" interaction scheme at our hands.
// Nevertheless: we still check for a matching reply, but can
// not wait for another one suddenly popping up from the
// HTTP tunnel.
//
//
// First, pull off the reply message header of the
// XDR stream. In case we also received a verifier
// from the server and this verifier was invalid, broken
// or tampered with, we will get an
// OncRpcAuthenticationException right here, which will
// propagate up to the caller. If the server reported
// an authentication problem itself, then this will
// be handled as any other rejected ONC/RPC call.
//
// While that sounds easy, it is hard work in the face
// of decoding the Base64 encoded data. But we have the
// decoding HTTP/XDR stream to delegate all this dirty
// work to...
//
receivingXdr.beginDecoding();
//
// Make sure that we got an okay from the web server and
// some data.
//
responseCode = httpClient.getResponseCode();
if ( (responseCode < 200) || (responseCode >= 300) ) {
// FIXME
throw(new OncRpcException(OncRpcException.RPC_FAILED,
"HTTP tunnel response error "
+ responseCode));
}
//
// Pull off the RPC header from the HTTP stream.
//
replyHeader.xdrDecode(receivingXdr);
//
// Only deserialize the result, if the reply matches the
// call. Otherwise skip this record.
//
if ( replyHeader.messageId != callHeader.messageId ) {
receivingXdr.endDecoding();
// FIXME: CHECKME exception code
throw(new OncRpcException(OncRpcException.RPC_WRONGMESSAGE));
}
//
// Make sure that the call was accepted. In case of unsuccessful
// calls, throw an exception, if it's not an authentication
// exception. In that case try to refresh the credential first.
//
if ( !replyHeader.successfullyAccepted() ) {
receivingXdr.endDecoding();
//
// Check whether there was an authentication
// problem. In this case first try to refresh the
// credentials.
//
if ( (refreshesLeft > 0)
&& (replyHeader.replyStatus
== OncRpcReplyStatus.ONCRPC_MSG_DENIED)
&& (replyHeader.rejectStatus
== OncRpcRejectStatus.ONCRPC_AUTH_ERROR)
&& (auth.canRefreshCred()) ) {
continue Refresh;
}
//
// Nope. No chance. This gets tough.
//
throw(replyHeader.newException());
}
result.xdrDecode(receivingXdr);
//
// Free pending resources of buffer and exit the call loop,
// returning the reply to the caller through the result
// object.
//
receivingXdr.endDecoding();
return;
} catch ( InterruptedIOException e ) {
//
// In case our time run out, we throw an exception.
//
throw(new OncRpcTimeoutException());
} catch ( IOException e ) {
//
// Argh. Trouble with the transport. Seems like we can't
// receive data. Gosh. Go away!
//
throw(new OncRpcException(OncRpcException.RPC_CANTRECV,
e.getLocalizedMessage()));
}
} // for ( refreshesLeft )
}
/**
* Set the character encoding for (de-)serializing strings.
*
* @param characterEncoding the encoding to use for (de-)serializing strings.
* If null
, the system's default encoding is to be used.
*/
public void setCharacterEncoding(String characterEncoding) {
sendingXdr.setCharacterEncoding(characterEncoding);
receivingXdr.setCharacterEncoding(characterEncoding);
}
/**
* Get the character encoding for (de-)serializing strings.
*
* @return the encoding currently used for (de-)serializing strings.
* If null
, then the system's default encoding is used.
*/
public String getCharacterEncoding() {
return receivingXdr.getCharacterEncoding();
}
/**
* Returns the host name of the HTTP server we are connected to.
*
* @return host name.
*/
public String getHostname() {
return hostname;
}
/**
* Returns the port of the HTTP server we are connected to.
*
* @return port number.
*/
public int getHttpPort() {
return httpPort;
}
/**
* DNS name of host where to contact HTTP server. Note that we can not
* use an InetAddress
here as we have to support virtual
* hosting, where several DNS names share the same IP address.
*/
private String hostname;
/**
* Port number where the HTTP server can be contacted.
*/
private int httpPort;
/**
* Path of cgi program redirecting ONC/RPC calls to the appropriate
* ONC/RPC servers.
*/
private String cgiHandlerPath;
/**
* DNS name of ONC/RPC server receiving the calls.
*/
private String oncrpcHostname;
/**
* Transport protocol to be used by the other end of the tunnel to
* contact the ONC/RPC server.
*/
private int oncrpcProtocol;
/**
* The HTTP client responsible for handling the HTTP connection. It is
* a classic example of delegation, isn't it?
*/
private HttpClientConnection httpClient;
/**
* FIXME: use the right encoding stream!
* XDR encoding stream used for sending requests via UDP/IP to an ONC/RPC
* server.
*/
private XdrBufferEncodingStream sendingXdr;
/**
* XDR decoding stream used when receiving replies via an HTTP tunnel
* from an ONC/RPC server.
*/
private XdrHttpDecodingStream receivingXdr;
}
// End of OncRpcHttpClient.java