org.acplt.oncrpc.web.HttpClientConnection Maven / Gradle / Ivy
Show all versions of remotetea-oncrpc Show documentation
/*
* $Header: /home/harald/repos/remotetea.sf.net/remotetea/src/org/acplt/oncrpc/web/HttpClientConnection.java,v 1.1 2003/08/13 12:03:45 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.web;
import java.io.IOException;
import java.io.OutputStream;
import java.io.BufferedOutputStream;
import java.io.InputStream;
import java.io.BufferedInputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.net.ProtocolException;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import org.acplt.oncrpc.OncRpcConstants;
/**
* The class HttpClientConnection
provides a simple HTTP/1.1
* compliant connection from a client to an HTTP server. This class does not
* provide a full-blown HTTP connection, but it is rather optimized for what
* ONC/RPC clients need in order to tunnel ONC remote procedure calls through
* ordinary HTTP connections, thus penetrating firewalls.
*
* A HttpClientConnection
is not that clever as you would
* first expect. Rather you have to do some things for yourself, like
* reconnecting dropped connections, and so on. While this sometimes result
* in more labour on the caller's shoulders, this keeps resource wasting at
* a minimum, and gives you full control over redirections and other mess --
* you do want full control, right?.
*
*
For this reason, for instance, an HttpClientConnection
* does not buffer the whole request before sending it to the server but
* rather relies on the caller to supply the right content-length information.
* This avoids unnecessary double buffering but instead creates the bas64
* encoded content on-the-fly.
*
*
Of course, this client connection object does not touch the content,
* it just suplies the pipe to swallow the data.
*
* @version $Revision: 1.1 $ $Date: 2003/08/13 12:03:45 $ $State: Exp $ $Locker: $
* @author Harald Albrecht
*/
public class HttpClientConnection {
/**
* Default port where HTTP servers listen for incomming requests. This is
* just a convenience definition to make the code more readable.
*/
public final static int HTTP_DEFAULTPORT = 80;
/**
* Constructs a new HttpClientConnection
. The port used on
* the HTTP server side is the default HTTP port, 80.
*
* @param hostname name (DNS name or IP dotted address) of host running
* a HTTP server to which we want to connect to.
*/
public HttpClientConnection(String hostname) {
this(hostname, HTTP_DEFAULTPORT);
}
/**
* Constructs a new HttpClientConnection
.
*
* @param hostname name (DNS name or IP dotted address) of host running
* a HTTP server to which we should connect to.
* @param port Port number where the HTTP server can be contacted.
*/
public HttpClientConnection(String hostname, int port) {
this.hostname = hostname;
this.port = port;
//
// The connection is closed, that is, dead.
//
mode = HTTP_DEAD;
}
/**
* Closes the connection to the HTTP server and frees up some resources.
* After calling close
it is still possible to open a new
* connection to the HTTP server once again.
*/
public void close() {
if ( socket != null ) {
//
// Close the input and output streams, then close the
// socket. Catch all exceptions when closing each object
// individually, so we can clean up all three despite
// them throwing nasty exceptions at us...
//
try {
in.close();
} catch ( IOException e ) { }
try {
out.close();
} catch ( IOException e ) { }
try {
socket.close();
} catch ( IOException e ) { }
socket = null;
out = null;
in = null;
}
mode = HTTP_DEAD;
}
/**
* Starts a new HTTP "POST" request and sends all necessary HTTP header
* fields. Next, the caller can send lots of content using the
* {@link #writeContentBytes} method. Finally, to finish the request
* he has to call the {@link #endPostRequest} method.
*
* @param path Path to server object which handles the POST request.
* For instance, this can be a CGI script (although it better should
* not be one, except in the case of FAST CGI).
* @param mimeType MIME-classified type of content to be sent.
* @param contentLength Length of content to be sent. If negative, the
* length is not known in advance. In this case, keeping the connection
* alive is not possible, so callers should avoid this situation,
* if possible.
*
* @exception IOException if an I/O exception occurs when sending the
* HTTP headers. In this case the connection is closed automatically
* and the caller must not send any content. However, the caller
* is free to give the request another try by calling
* beginEncoding
again, thus opening a new HTTP connection.
*/
public void beginPostRequest(String path,
String mimeType, int contentLength)
throws IOException {
//
// Make sure that the connection is open. This will also handle the
// case that the HTTP server or proxy can not keep connections alive,
// either because it can not per se or the client does not know the
// requests's content length in advance (bad, bad, very bad!).
//
connect();
//
// Remember how many content bytes we need to see before finishing
// the request and then enter "sending" mode.
//
remainingContentLength = contentLength;
mode = HTTP_SENDING;
//
// Set the socket timeout, so we don't hang around forever waiting
// for an answer or to get rid of our content.
//
socket.setSoTimeout(timeout);
//
// Send method header and all the other useful headers.
//
if ( useProxy ) {
writeln("POST http://" + hostname + path + " HTTP/1.1");
} else {
writeln("POST " + path + " HTTP/1.1");
}
//
// Now send all the interesting headers...
// This first involves indicating the host we intended to contact
// while going through an HTTP proxy. Also send some interesting
// agent information about us...
//
if ( !useProxy ) {
writeln("Host: " + hostname);
}
writeln("User-Agent: " + userAgentId);
//
// Try to keep the connection alive.
//
if ( useProxy ) {
writeln("Proxy-Connection: keep-alive");
} else {
writeln("Connection: keep-alive");
}
//
// Ensure that proxies do not cache the POST request. On the other
// side of the wire, the HTTP server -- respective the redirecting
// CGI -- is responsible for returning appropriate cache control
// headers.
//
writeln("Cache-Control: no-cache, no-store, private");
writeln("Pragma: no-cache");
//
// Finish headers with a description of the content that will follow
// within this POST request. If the exact content-length is not known
// in advance we have to fall back to the old close-the-line principle.
//
// Note: this is currently not possible because Java does not allow
// us to close only one side of the socket connection. Half-closes
// have only been introduced lately with the JDK1.3 -- sloooooowly,
// Sun is learning what is needed for an "Internet Platform". As if
// that wasn't clear from the beginning... did no-one read the
// BSD socket API for a start?! And what about Mr. Tanenbaum?!
//
writeln("Content-Type: " + mimeType);
if ( contentLength > 0 ) {
writeln("Content-Length: " + contentLength);
} else {
throw(new ProtocolException(
"ONC/RPC HTTP-tunnel POST needs content length to keep the connection alive"));
}
//
// Finish the header section of the HTTP request. This is marked
// according to the HTTP specification by an empty line. The next
// thing following is content...
//
writeln("");
//
// Let the real content roll -- or were that the "good times?"
//
}
/**
* Send (part) of the content to the HTTP server. Note that the output
* is done unbuffered, so callers should write their content in large
* chunks to avoid the calling overhead for sending data.
*
* @param bytes The data.
* @param offset Start offset in the data.
* @param length Number of bytes to write.
*
* @exception RuntimeException if too much content was sent.
* @exception IOException if an I/O error occurs.
* @exception NullPointerException if bytes
is
* null
.
* @exception IndexOutOfBoundsException if offset
is negative,
* or length
is negative, or offset + length
is
* greater than the length of the array bytes
.
*/
public void writeContentBytes(byte [] bytes, int offset, int length)
throws IOException {
if ( mode != HTTP_SENDING ) {
throw(new ProtocolException(
"ONC/RPC HTTP tunnel not in sending mode"));
}
//
// Check incomming parameters...
//
if ( bytes == null) {
throw(new NullPointerException());
} else if ( (offset < 0)
|| (offset > bytes.length)
|| (length < 0)
|| ((offset + length) > bytes.length)
|| ((offset + length) < 0) ) {
throw(new IndexOutOfBoundsException());
} else if ( length == 0 ) {
return;
}
//
// First check that not too much amount of content gets sent.
// (Looks like this should be a code template for marketing!)
//
if ( remainingContentLength >= 0 ) {
remainingContentLength -= length;
if ( remainingContentLength < 0 ) {
close();
throw(new ProtocolException(
"ONC/RPC HTTP tunnel received too much content"));
}
}
//
// Whow! Finally write some bytes... In case we get an I/O error,
// we terminate the connection and rethrow the I/O exception.
//
try {
out.write(bytes, offset, length);
} catch ( IOException e ) {
close();
throw(e);
}
}
/**
* Ends the HTTP "POST" request. The next logical step for a caller is
* then to call ... #FIXME
*
* @throws IOException The post data of the post request could not be flushed.
*/
public void endPostRequest()
throws IOException {
if ( remainingContentLength > 0 ) {
//
// As not all content has been sent, abort the connection the
// hard way and throw an exception to notify the caller.
//
close();
throw(new ProtocolException(
"ONC/RPC HTTP tunnel received not enough content"));
}
try {
out.flush();
} catch ( IOException e ) {
//
// If flushing the connection results in an I/O exception, tear
// down the connection before rethrowing the exception. This allows
// the caller to reconnect and retry the POST.
//
close();
throw(e);
}
mode = HTTP_IDLE;
}
/**
* Handle options sent by the HTTP server.
*
*
Currently the following options are handled by this class:
*
* - Content-Length -- length of content following the header section
* - Content-Type -- type of content sent
* - Proxy-Connection -- handle keep-alive request
* - Connection -- handle keep-alive request
* - Transfer-Encoding -- transfer encoding choosen by HTTP server
*
*
* @param option Name of option sent by HTTP server.
* @param value Value of option.
*/
protected void handleOption(String option, String value) {
// FIXME System.out.println("OPTION: \"" + option + "\" = \"" + value + "\"");
if ( "Content-Length".equalsIgnoreCase(option) ) {
try {
remainingContentLength = Integer.parseInt(value);
} catch ( NumberFormatException e ) {
}
} else if ( "Content-Type".equalsIgnoreCase(option) ) {
contentType = value;
} else if ( "Proxy-Connection".equalsIgnoreCase(option) ) {
if ( useProxy ) {
keepAlive = "Keep-Alive".equalsIgnoreCase(value);
}
} else if ( "Connection".equalsIgnoreCase(option) ) {
if ( !useProxy ) {
keepAlive = "Keep-Alive".equalsIgnoreCase(value);
}
} else if ( "Transfer-Encoding".equalsIgnoreCase(option) ) {
chunkedTransfer = "chunked".equalsIgnoreCase(value);
}
}
/**
* Read the HTTP headers sent by the servers and also parse them at the
* same time.
*
* @return HTTP status code.
*
* @throws IOException if an invalid HTTP header has been received.
*/
private int readHeaders()
throws IOException {
//
// Reset/init section.
//
keepAlive = false;
remainingContentLength = 0;
chunkedTransfer = false;
remainingChunkLength = 0;
contentType = null;
//
// Read response line, but handle the dreaded HTTP/1.1 100 Continue
// server response.
//
String [] param;
int httpStatus;
for ( ;; ) {
//
// First, read in the response line, which contains the HTTP version
// spoken by the server, the status code, and a reason phrase.
//
param = new String[1];
if ( !readHeaderLine(param)
|| !param[0].startsWith("HTTP/") ) {
throw(new IOException("Invalid HTTP header"));
}
String header = param[0];
//
// Retrieve the status code from the HTTP header line. This involves
// finding the end of the HTTP/x.x string and skipping all spaces
// until we reach the HTTP status code in the form of XYZ.
// Yes, we are not interested in the HTTP protocol spoken by the server.
//
int index = 0;
int len = header.length();
for ( ; (index < len) && (header.charAt(index) != ' ') ; ++index ) {
// empty
}
for ( ; (index < len) && (header.charAt(index) == ' ') ; ++index ) {
// empty
}
try {
responseCode = 300; // #FIXME?
httpStatus = Integer.parseInt(header.substring(index, index + 3));
responseCode = httpStatus;
} catch ( NumberFormatException e ) {
throw(new IOException("Invalid HTTP header"));
}
//
// If it's not a "100 Continue", then we can proceed reading
// header lines. Otherwise we expect to see another HTTP response
// line.
//
if ( responseCode != 100 ) {
break;
}
}
//
// Now parse the following options within the HTTP header. Every
// non-empty line contains an HTTP option, which is handed over to
// the handleOption method for implementation-specific behaviour.
//
param = new String[2];
for ( ;; ) {
if ( readHeaderLine(param) ) {
handleOption(param[0], param[1]);
} else {
//
// End of HTTP header section reached, so leave the loop.
//
break;
}
}
//
// Some final sanity checks: if the server does not know how long
// the content it sends is going to be, it sends a negative length.
// In this case we can not keep the connection alive (and in fact
// the server should not have been responded with such a header, but
// we gracefully ignore this here).
//
if ( remainingContentLength <= 0 ) {
keepAlive = false;
remainingContentLength = -1; // means "don't know"
}
//
// Done. Return the status of the HTTP request returned in this reply.
//
return httpStatus;
}
/**
* Read in a header line coming over the HTTP connection from the server.
*
* @param keyvalue An array with room for either exactly one or two
* strings, receiving the header option and optionally its value. If
* only room for a single return string is supplied, then
* readHeaderLine
will only read in the header line (like
* the first HTTP header) without separating the options's value from
* its name. If a header option has no option, then null
* is returned as the value string.
*
* @return false
if the end of the headers has been reached.
*
* @throws IOException IO error reading from the internal input stream.
*/
private boolean readHeaderLine(String [] keyvalue)
throws IOException {
int headerSize = headerLine.length;
int index = 0;
boolean option = keyvalue.length > 1;
//
// Now parse the next header line.
//
int ch;
int nextch;
int colon = -1;
ReadLine:
while ( (ch = in.read()) >= 0 ) {
switch ( ch ) {
//
// For header options in form of key: value remember the position.
// Note that the use the invariant that the index can not be
// negative. So if the colon position is not negative, it has
// already been set and we must not set it again, as, for instance,
// the value of the option might also contains colons.
//
case ':':
if ( colon < 0 ) {
colon = index;
}
break;
//
// Replace HT with SP
//
case '\t':
ch = ' ';
break;
//
// Swallow CRLF. As we speak HTTP/1.1 (or at least we could), we
// need to support header lines folded onto multiple lines.
//
case '\r':
case '\n':
//
// Check for a continuation line, which is indicated by a
// new line beginning with either a space or a horizontal tab.
//
in.mark(1);
nextch = in.read();
if ( (ch == '\r') && (nextch == '\n') ) {
//
// Okay. This *is* a CRLF. Now let's look for a continuation
// indication...
//
in.mark(1);
nextch = in.read();
if ( (nextch != ' ') && (nextch != '\t') ) {
//
// Nope. This is the final line of a header line. So we need
// to back up so the char can be read on the next round.
//
in.reset();
break ReadLine;
}
} else {
in.reset();
}
//
// This is either crap or a continuation line, so we replace
// the crap or the CRLF and the following SP or HT (in case of
// a continuation line) with a single SP.
//
ch = ' ';
break;
}
//
// Add character to header line buffer, growing the buffer as
// needed.
//
if ( index >= headerSize ) {
char [] newHeaderLine = new char[headerSize * 2];
System.arraycopy(headerLine, 0, newHeaderLine, 0, headerSize);
headerLine = newHeaderLine;
headerSize *= 2;
}
headerLine[index++] = (char) ch;
}
//
// End of headers reached (an empty line)?
//
if ( index == 0 ) {
return false;
}
//
// We are done. Return the header string, constructed from the buffer.
// In case we're dealing with options which have values and parsing
// is not disabled, we first separate the option and its value and
// then return as two separate strings.
//
while ( (--index > 0) && (headerLine[index] <= ' ') ) {
// empty
}
++index;
if ( option ) {
if ( colon <= 0 ) {
//
// Only "option", but not "option: value"
//
keyvalue[0] = new String(headerLine, 0, index);
keyvalue[1] = null;
} else {
keyvalue[0] = new String(headerLine, 0, colon);
while ( (++colon < index) && (headerLine[colon] <= ' ') ) {
// empty
}
keyvalue[1] = new String(headerLine, colon, index - colon);
}
} else {
keyvalue[0] = new String(headerLine, 0, index);
}
return true;
}
/**
* Read exactly one line, termined by CRLF or either CR or LF, and return
* it.
*
* @return Line without the terminating CR, LF or CRLF.
*
* @throws IOException IO error reading from the internal input stream.
*/
private String readLine()
throws IOException {
int headerSize = headerLine.length;
int index = 0;
//
// Now parse the next header line.
//
int ch;
int nextch;
ReadLine:
while ( (ch = in.read()) >= 0 ) {
switch ( ch ) {
//
// Swallow CRLF.
//
case '\r':
case '\n':
in.mark(1);
nextch = in.read();
if ( (ch == '\r') && (nextch == '\n') ) {
//
// Okay. This *is* a CRLF.
//
break ReadLine;
} else {
in.reset();
}
//
// This is either crap or a continuation line, so we replace
// the crap or the CRLF and the following SP or HT (in case of
// a continuation line) with a single SP.
//
ch = ' ';
break;
}
//
// Add character to header line buffer, growing the buffer as
// needed.
//
if ( index >= headerSize ) {
char [] newHeaderLine = new char[headerSize * 2];
System.arraycopy(headerLine, 0, newHeaderLine, 0, headerSize);
headerLine = newHeaderLine;
headerSize *= 2;
}
headerLine[index++] = (char) ch;
}
//
// End of headers reached?
//
return new String(headerLine, 0, index);
}
/**
* Begin receiving the content sent by the HTTP server. This
* method blocks until at least the HTTP header section has been received
* or until the connection times out.
*
* @return HTTP server response code (status code).
*
* @throws IOException Reading the HTTP header failed.
*/
public int beginDecoding()
throws IOException {
mode = HTTP_RECEIVING;
finalChunkSeen = false;
//
// Now read the HTTP headers sent by the server. If the response code
// is not okay, we end decoding immediately. This will either result
// in the connection getting closed of the content skipped, if the
// connection can be kept alive.
//
readHeaders();
switch ( responseCode ) {
case 200:
//
// Accepted request...
//
break;
default:
//
// All other response codes: not accepted!
//
endDecoding();
}
return responseCode;
}
/**
* Returns the content type (MIME type, charset, etc.).
*
* @return content type
*/
public String getContentType() {
return contentType;
}
/**
* Read content sent by the HTTP server. This method also handles the
* HTTP/1.1 chunked transfer encoding if the HTTP server insisted on using
* it. If only chunked transfer encoding has been introduced with the
* first official 0.9 protocol version of HTTP, it would have made sending
* ONC/RPC requests much easier, because we would not need to buffer the
* while request before sending it just to know its exact length.
* Unfortunately, chunking has only been introduced lately so we can not
* expect servers and especially proxies to handle it. Sigh.
*
* @param buffer Buffer to receive the content sent by the HTTP server.
* @param offset Start offset in the buffer.
* @param length Number of bytes to receive.
*
* @return The number of bytes in total read.
*
* @exception ProtocolException if not enough content was available (the
* caller attempted to read more data than available) or if we received
* junk information violating the HTTP protocol specification.
* @exception IOException if an I/O error occurs.
* @exception NullPointerException if bytes
is
* null
.
* @exception IndexOutOfBoundsException if offset
is negative,
* or length
is negative, or offset + length
is
* greater than the length of the array bytes
.
*/
public int readContentBytes(byte [] buffer, int offset, int length)
throws IOException {
int bytesread;
int total = 0;
if ( mode != HTTP_RECEIVING ) {
throw(new ProtocolException(
"ONC/RPC HTTP tunnel not in receiving mode"));
}
//
// Check incomming parameters...
//
if ( buffer == null ) {
throw(new NullPointerException());
} else if ( (offset < 0)
|| (offset > buffer.length)
|| (length < 0)
|| ((offset + length) > buffer.length)
|| ((offset + length) < 0) ) {
throw(new IndexOutOfBoundsException());
} else if ( length == 0 ) {
return 0;
}
//
// First check that there is still enough content to read. In case
// the HTTP server did not indicate the content length, we return
// as much as possible until we hit the "end of file". Note that
// in case of chuncked transfers, we don't know the content length
// in advance too, so we read 'til we drop.
//
// In contrast to Java's read() from the io package, we block until
// we either got the specified amout of content or until we reach
// eof.
//
if ( remainingContentLength >= 0 ) {
//
// Do not decrement remainingContentLength here to avoid getting
// out of sync in case of exceptions...
//
if ( remainingContentLength < length ) {
close();
throw(new ProtocolException(
"ONC/RPC HTTP tunnel has not enough content available"));
}
}
//
// Whow! Finally read some bytes... In case we get an I/O error,
// we terminate the connection and rethrow the I/O exception.
// But beginning with HTTP/1.1 reading some bytes can actually be not
// that easy as expected from earlier protocol versions, as we now
// probably have to deal with transfer encodings. Currently, we only
// support chunked transfers.
//
if ( chunkedTransfer ) {
int toRead;
//
// Beginning with HTTP/1.1 we need to handle chunked transfers,
// where the sender splits the content into chunks without telling
// the receiver the overall total length, but only how long an
// individual chunk is going to be. Advantage: the sender does not
// need to buffer the content in order to find out the total length,
// but can instead use a fixed-sized buffer and split the content
// into appropriate-sized chunks.
//
try {
for ( ;; ) {
//
// If the previous chunk has been completely read, we now
// need to read in the length of the following chunk. If
// we're currently in the middle of a chunk, we can skip
// this step and immediately proceed to slurp chunk content.
//
if ( remainingChunkLength <= 0 ) {
if ( finalChunkSeen ) {
throw(new ProtocolException(
"ONC/RPC HTTP tunnel has not enough content available"));
}
//
// Next chunk or first chunk:
// the length of the following chunk is encoded as
// an hex integer followed by a line break. If we read
// a zero length chunk, we've reached the end of the
// road. This is only acceptable if no more data needs
// to be read.
//
String hexLen = readLine();
//
// This is ridiculous: parseInt does not like white
// space, and Apache sends them after the chunk length
// and before the line termination CRLF. So we need
// to trim the string.
//
try {
remainingChunkLength = Integer.parseInt(hexLen.trim(), 16);
if ( remainingChunkLength < 0 ) {
throw(new NumberFormatException("must not be negative"));
}
} catch ( NumberFormatException e ) {
throw(new ProtocolException(
"HTTP chunking transfer protocol violation: invalid chunk length \""
+ hexLen + "\""));
}
if ( remainingChunkLength == 0 ) {
finalChunkSeen = true;
//if ( length > 0 ) {
//
// If we got here, we reached the last chunk but
// the caller wanted even more data.
//
// throw(new ProtocolException(
// "ONC/RPC HTTP tunnel has not enough content available"));
//}
return total;
}
}
//
// Now read in as many data as the caller wants or as many
// as the current chunk contains, whichever is less. Note
// that the read method will only return *up to* the amount
// of data specified, so it can perfectly return with less
// data than expected if the I/O layer, and especially the
// TCP stack decides to do so although there's still data
// "in the pipe". To cope with this situation, we turn one
// round after another as long as there is still data for
// the current chunk outstanding.
//
while ( remainingChunkLength > 0 ) {
toRead = length <= remainingChunkLength ?
length : remainingChunkLength;
bytesread = in.read(buffer, offset, toRead);
if ( bytesread < 0 ) {
throw(new ProtocolException(
"ONC/RPC HTTP tunnel has not enough content available"));
}
offset += bytesread;
total += bytesread;
length -= bytesread;
remainingChunkLength -= bytesread;
if ( length <= 0 ) {
//
// In case we also reached the end of the current
// chunk, swallow the CRLF terminating the chunk.
// Otherwise we would get into trouble when the
// next read expects to see a chunk length
// indication.
//
if ( remainingChunkLength <= 0 ) {
readLine();
}
return total;
}
}
//
// Swallow trailing CRLF terminating each chunk of data,
// then start over with the next chunk, because there is
// still data to be read.
//
readLine();
}
} catch ( IOException e ) {
close();
throw(e);
}
}
//
// Handle protocol versions HTTP/0.9, HTTP/1.0, or non-chunked
// transfers with HTTP/1.1. This is the simple case, where we only
// slurp in bytes, bytes, bytes...
//
try {
while ( length > 0 ) {
bytesread = in.read(buffer, offset, length);
if ( bytesread < 0 ) {
//
// In case we reach eof, the read() method will first
// return all data up to eof, and then, on the next call,
// -1 to indicate eof. We only can reach eof when the
// server closed its sending side of the connection. When
// we see an eof, we make sure that the caller did not
// expected to get more data when we got.
//
if ( remainingContentLength >= 0 ) {
throw(new ProtocolException(
"ONC/RPC HTTP tunnel has not enough content available"));
}
break;
}
total += bytesread;
offset += bytesread;
length -= bytesread;
}
} catch ( IOException e ) {
//
// In case of I/O problems, and especially communication timeouts,
// close the connection and rethrow the exception to notify the
// caller.
//
close();
throw(e);
} finally {
//
// Don't forget to update the counter bean heads...
//
if ( remainingContentLength >= 0 ) {
remainingContentLength -= total;
}
}
//
// Phew! Done with the easy case, now return the amount of data
// received from the HTTP server in this turn.
//
return total;
}
/**
* Returns amount of content still available (to be read). This always
* shows the remaining amount and is updated whenever content is read
* using {@link #readContentBytes}.
*
* @return Amount of content available.
*/
public int getRemainingContentLength() {
return remainingContentLength;
}
/**
*
* This method silently discards any unread content, if the caller has
* yet not read all content.
*
* @throws IOException if reading remaining bytes fails.
*/
public void endDecoding()
throws IOException {
//
// Make sure that we slurp all data yet unread to keep keep-alive
// connections alive (I love the English tongue... but maybe this is
// just the result of my German tongue and syntax going crazy, so
// Mark Twain was right. No, Mark Twain did not invent the scanner API!)
//
if ( chunkedTransfer ) {
if ( keepAlive && !finalChunkSeen ) {
//
// Only makes sense if the connection can be kept alive, otherwise
// we don't want to bother ourselves slurping junk data...
//
int chunkLength;
long bytesSkipped;
try {
//
// First, get rid of the current chunk, if there is any.
//
while ( remainingChunkLength > 0 ) {
bytesSkipped = in.skip(remainingChunkLength);
if ( bytesSkipped < 0 ) {
throw(new IOException("Could not skip chunk"));
}
remainingChunkLength -= bytesSkipped;
}
//
// Then dispose any other chunks...
//
String hexLen;
while ( !finalChunkSeen ) {
//
// Read next chunk header, then flush chunk data, including
// the CRLF terminating each chunk.
//
hexLen = readLine();
chunkLength = Integer.parseInt(hexLen, 16);
if ( chunkLength < 0 ) {
throw(new NumberFormatException("must not be negative"));
}
if ( chunkLength == 0 ) {
break;
}
while ( chunkLength > 0 ) {
bytesSkipped = in.skip(remainingChunkLength);
if ( bytesSkipped < 0 ) {
throw(new IOException("Could not skip chunk"));
}
chunkLength -= bytesSkipped;
}
readLine();
}
} catch ( Exception e ) {
//
// Got in any trouble? Then kill the connection.
//
close();
}
}
} else if ( keepAlive ) {
//
// Skip remaining unread content, if the content length is known
// in advance. Otherwise drop the connection.
//
if ( remainingContentLength > 0 ) {
long bytesSkipped;
while ( remainingContentLength > 0 ) {
bytesSkipped = in.skip(remainingContentLength);
if ( bytesSkipped < 0 ) {
close();
break;
}
remainingContentLength -= bytesSkipped;
}
} else if ( remainingContentLength < 0 ) {
close();
}
}
//
// Indicate new mode, if the connection is still alive, or close it
// if it can not kept alive.
//
if ( mode != HTTP_DEAD ) {
if ( !keepAlive ) {
close();
} else {
mode = HTTP_IDLE;
}
}
}
/**
* Set the timout for sending or receiving information to/from the HTTP
* server.
*
* @param milliseconds Timeout in milliseconds.
*/
public void setTimeout(int milliseconds) {
if ( milliseconds <= 0 ) {
throw(new IllegalArgumentException("timeouts must be positive."));
}
timeout = milliseconds;
}
/**
* Retrieve the current timeout set for remote procedure calls. A timeout
* of zero indicates batching calls (no reply message is expected).
*
* @return Current timeout.
*/
public int getTimeout() {
return timeout;
}
/**
* Retrieve the response code of the last remote procedure call via HTTP.
*
* @return The numbver of the response code.
*/
public int getResponseCode() {
return responseCode;
}
/**
* @return True if the keep alive flag is set, false otherwise.
*/
public boolean getKeepAlive() {
return keepAlive;
}
/**
* Connects to the HTTP server. In case an HTTP proxy has been configured,
* this connects to the proxy instead.
*
*
If the connection is in keep-alive mode and already open, then the
* connection is reused.
*
* @exception SecurityException if a security manager exists and its
* checkConnect
method does not allow the connect
* operation.
* @throws IOException Creating and connecting a new socket failed, respectively.
*/
private void connect()
throws IOException {
//
// Only connect, if we don't already have a socket at hand. Otherwise
// we reuse the old connection, if that is okay.
//
if ( socket != null ) {
if ( keepAlive ) {
return;
}
close();
}
//
// Check that we are allowed to connect to the HTTP server. Note that
// this is the barrier which keeps callers from connecting to anywhere
// through a proxy server -- note that connection to the proxy is
// always done as a privileged operation, so we must be cautious here.
//
SecurityManager security = System.getSecurityManager();
if ( security != null ) {
security.checkConnect(hostname, port);
}
//
// If we had already successfully contacted the proxy HTTP server
// during the last request, we contact it once again. This way, we
// don't need to go through all the system's property hasle, etc.
//
if ( cachedProxyHost != null ) {
socket = new Socket(cachedProxyHost, cachedProxyPort);
useProxy = true;
} else {
//
// FIXME: support for non-proxied servers...
//
//
// Check for proxy server. If one has configured, this is the
// one to connect to. The proxy then will connect to the real
// http server.
//
final String proxyHost = getProxyHost();
if ( proxyHost != null ) {
final int proxyPort = getProxyPort();
try {
AccessController.doPrivileged(new PrivilegedExceptionAction() {
public Object run()
throws IOException {
socket = new Socket(proxyHost, proxyPort);
useProxy = true;
cachedProxyHost = proxyHost;
cachedProxyPort = proxyPort;
return null; // return value is not used by caller
}
} );
} catch ( PrivilegedActionException e ) {
//
// Okay, now we can really try to connect to the HTTP server.
//
useProxy = false;
socket = new Socket(InetAddress.getByName(hostname), port);
}
} else {
//
// Okay, now we can really try to connect to the HTTP server.
//
useProxy = false;
socket = new Socket(InetAddress.getByName(hostname), port);
}
}
//
// It might help to disable Nagle, so that packets are not delayed
// after we've handed it over to the network stack before going to
// the wire.
//
socket.setTcpNoDelay(true);
//
// Get all the streams we like to work with. We do buffer both the
// input and output streams to make the whole thing perform better.
//
out = new BufferedOutputStream(socket.getOutputStream(), 4096);
in = new BufferedInputStream(socket.getInputStream(), 4096);
//
// FIXME??
keepAlive = true;
}
/**
* Writes an ASCII string to the HTTP server (this is buffered first).
*
* @param s String to write.
*
* @throws IOException Writing the passed string to the internal output stream failed.
*/
private void write(String s)
throws IOException {
int slen = s.length();
int sindex = 0;
int index;
int blocklen = asciiBuffer.length;
int len;
while ( slen > 0 ) {
len = slen > blocklen ? blocklen : slen;
for ( index = 0; index < len; ++index ) {
asciiBuffer[index] = (byte) s.charAt(sindex++);
}
slen -= len;
out.write(asciiBuffer, 0, len);
}
}
/**
* Writes an ASCII string and appends a line termination in the form of
* CR followed by LF.
*
* @param s String to write.
*
* @throws IOException Writing the passed string or the new line character to the internal output stream failed.
*/
private void writeln(String s)
throws IOException {
write(s);
out.write(CRLF);
}
/**
* Retrieves the host name where the HTTP proxy server resided from the
* system properties.
*
* @return Name of proxy host or null
if no proxy has been
* configured.
*/
private String getProxyHost() {
String proxyHost = (String) AccessController.doPrivileged(
new GetPropertyPrivilegedAction("http.proxyHost"));
if ( proxyHost == null ) {
proxyHost = (String) AccessController.doPrivileged(
new GetPropertyPrivilegedAction("proxyHost"));
}
return proxyHost;
}
/**
* Retrieves the port number where the HTTP proxy server resides from
* the system properties. If no port number has been configured for the
* HTTP proxy, then the default http port number of 80 is returned.
*
* @return Port number of HTTP proxy server.
*/
private int getProxyPort() {
String proxyPort = (String) AccessController.doPrivileged(
new GetPropertyPrivilegedAction("http.proxyPort"));
if ( proxyPort == null ) {
proxyPort = (String) AccessController.doPrivileged(
new GetPropertyPrivilegedAction("proxyPort"));
}
if ( proxyPort != null ) {
//
// If something has been specified as the proxy port system
// property, try to convert this into a port number. If this
// fails, return the default port number of an HTTP server.
//
try {
return Integer.parseInt(proxyPort);
} catch ( NumberFormatException e ) {
}
}
return HTTP_DEFAULTPORT;
}
/**
* Host name of HTTP server to contact in the form of
* www.acplt.org
.
*/
private String hostname;
/**
* Port number where to contact the HTTP server. This defaults to the
* standard port where HTTP servers are expected: port 80.
*/
private int port = HTTP_DEFAULTPORT;
/**
* TCP/IP socket for communication with the HTTP server.
*/
private Socket socket;
/**
* Timeout (in milliseconds) for communication with an HTTP server.
*/
private int timeout = 30000;
/**
*
*/
private boolean keepAlive = true;
/**
* Indicates whether the HTTP server send its reply using the chunked
* transfer encoding.
*/
private boolean chunkedTransfer;
/**
* Contains the amount of data still to be read for the current chunk.
*/
private int remainingChunkLength;
/**
* Indicates whether the end-of-chunks chunk has been read (whow, another
* one for the Purlitzer price).
*/
private boolean finalChunkSeen;
/**
* Type of content sent by HTTP server.
*/
private String contentType;
/**
* Buffered output stream used for sending the HTTP headers. This stream
* is not used to send content in an HTTP request.
*/
private OutputStream out;
/**
*
*/
private InputStream in;
/**
* Contains the HTTP response code from the last request sent to the
* HTTP server.
*/
private int responseCode;
/**
* Buffer receiving ASCII characters from Unicode strings when sending
* (header) strings to the HTTP server.
*/
private byte [] asciiBuffer = new byte[1024];
/**
* Dynamically growing buffer used during header parsing.
*/
private char [] headerLine = new char[80];
/**
*
*/
private String userAgentId = "Not Mozilla, but RemoteTea v"
+ OncRpcConstants.REMOTETEA_VERSION_STRING;
/**
*
*/
private final static int HTTP_DEAD = 0;
private final static int HTTP_IDLE = 1;
private final static int HTTP_SENDING = 2;
private final static int HTTP_RECEIVING = 3;
/**
* Indicates sending/receiving mode of the HTTP connection.
*/
private int mode = HTTP_IDLE;
/**
* Indicates the amount of content data which still has to be sent or
* received.
*/
private int remainingContentLength;
/**
*
*/
public final static byte [] CRLF = { 13, 10 };
/**
* Indicates whether a proxy HTTP server needs to be contacted in order
* to reach the real HTTP server.
*/
private boolean useProxy = false;
/**
* Address of proxy host to contact instead of real HTTP server.
*/
private String cachedProxyHost;
/**
* Port number of proxy HTTP server.
*/
private int cachedProxyPort;
/**
*
*/
class GetPropertyPrivilegedAction implements PrivilegedAction {
/**
*
* @param property The name of the property that shall be read from
* the systems property map.
*/
public GetPropertyPrivilegedAction(String property) {
this.property = property;
}
/**
*
* @param property The name of the property that shall be read from
* the systems property map.
* @param defaultValue The default value of the property with passed name.
*/
public GetPropertyPrivilegedAction(String property, String defaultValue) {
this.property = property;
this.defaultValue = defaultValue;
}
/**
*
*/
public Object run() {
return System.getProperty(property, defaultValue);
}
/**
* The property to retrieve.
*/
private String property;
/**
* Default value or null
.
*/
private String defaultValue;
}
}
// End of HttpClientConnection.java