blob: 8a38a2645bc0fbe194e7bbd6a7f0716b3548dad4 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2002-2003 IBM Corporation, Beacon Information Technology Inc. and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM - Initial API and implementation
* BeaconIT - Initial API and implementation
*******************************************************************************/
package org.eclipse.wst.wsi.internal.core.monitor;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import org.eclipse.wst.wsi.internal.core.WSIConstants;
import org.eclipse.wst.wsi.internal.core.WSIException;
import org.eclipse.wst.wsi.internal.core.log.MessageEntry;
import org.eclipse.wst.wsi.internal.core.util.Utils;
/**
* Socket Handler.
*
* @author Peter Brittenham (peterbr@us.ibm.com)
* @version 1.0.1
*/
public class SocketHandler extends Thread
{
protected SocketConnection socketConnection;
protected SocketHandler pairedSocketHandler;
protected String connectionType;
protected int conversationID;
protected String targetHost;
protected int readTimeoutSeconds;
protected Socket inSocket;
protected Socket outSocket;
protected InputStream inputStream = null;
protected OutputStream outputStream = null;
protected boolean verbose = false;
protected boolean readTimedOut = false;
// I18N: 2003.02.26 modified by K.Nakagome@BeaconIT
private String mimeCharset = null;
private String xmlEncoding = null;
protected static final String CRLF = "\r\n";
protected static final String HTTP_100_CONTINUE =
"100 Continue".toUpperCase();
protected static final String CHUNKED =
"Transfer-Encoding: chunked".toUpperCase();
protected static final String CHUNKED_WITH_QUOTES =
"Transfer-Encoding: \"chunked\"".toUpperCase();
protected static final String CONTENT_LENGTH =
"Content-Length:".toUpperCase();
/**
* Create socket handler.
* @param socketConnection socket connection.
* @param connectionType connection type.
* @param conversationID conversation id.
* @param targetHost target host.
* @param inSocket in socket.
* @param outSocket out socket.
* @param readTimeoutSeconds read timeout seconds.
*/
public SocketHandler(
SocketConnection socketConnection,
String connectionType,
int conversationID,
String targetHost,
Socket inSocket,
Socket outSocket,
int readTimeoutSeconds)
{
this.socketConnection = socketConnection;
this.connectionType = connectionType;
this.conversationID = conversationID;
this.targetHost = targetHost;
this.inSocket = inSocket;
this.outSocket = outSocket;
this.readTimeoutSeconds = readTimeoutSeconds;
// ADD:
verbose =
socketConnection.getMonitor().getMonitorConfig().getVerboseOption();
start();
}
/**
* Set paired socket handler.
* @param pairedSocketHandler paired socket handler.
*/
public void setPairedSocketHandler(SocketHandler pairedSocketHandler)
{
this.pairedSocketHandler = pairedSocketHandler;
}
/**
* Send the data from the input socket to the output socket.
*/
public void run()
{
int readLen;
String readMsg;
MessageContext messageContext = null;
// Create read buffer
byte[] readBuffer = new byte[4096];
try
{
// Get the input and output streams
this.inputStream = this.inSocket.getInputStream();
this.outputStream = this.outSocket.getOutputStream();
// Process while the connection is active
// (NOTE: there might be more than 1 message per connection)
boolean connectionActive = true;
while (connectionActive)
{
// Reset all data
readLen = 0;
messageContext = new MessageContext();
// Read until message is complete
boolean messageComplete = false;
while (!messageComplete)
{
try
{
// DEBUG:
debug("run", "Read data from the input stream.");
// Read data from the input stream
readLen = inputStream.read(readBuffer, 0, readBuffer.length);
// Reset read timeout flag
readTimedOut = false;
// DEBUG:
debug("run", "readLen: " + readLen);
if (readLen == -1)
{
connectionActive = false;
messageComplete = true;
}
// If data was read, then check for 100 continue
else if (readLen > 0)
{
// If this is the first data that was read, then get the timestamp
if (messageContext.timestamp == null)
messageContext.timestamp = Utils.getTimestamp();
if (connectionType.equals(MessageEntry.TYPE_REQUEST))
{
byte[] toHost =
new String(
socketConnection.redirect.getToHost()
+ ":"
+ socketConnection.redirect.getToPort())
.getBytes();
String message = new String(readBuffer, 0, readLen);
int index = message.indexOf(CRLF + "Host: ");
if (index > -1)
{
index += 8;
int secondPart = message.indexOf(CRLF, index);
// Write the data to the output stream and then go format it
write(this.outputStream, readBuffer, 0, index);
write(this.outputStream, toHost, 0, toHost.length);
write(
this.outputStream,
readBuffer,
secondPart,
readLen - secondPart);
}
else
{
// Write the data to the output stream and then go format it
write(this.outputStream, readBuffer, 0, readLen);
}
}
else
{
// Write the data to the output stream and then go format it
write(this.outputStream, readBuffer, 0, readLen);
}
// DEBUG:
if (verbose)
{
String bufferString = new String(readBuffer, 0, readLen);
debug("run", "buffer as string: [" + bufferString + "]");
if (bufferString.length() <= 50)
debug(
"run",
"buffer as hexstring: ["
+ Utils.toHexString(bufferString)
+ "]");
else
debug(
"run",
"buffer as hexstring: ["
+ Utils.toHexString(bufferString.substring(0, 50))
+ " ...]");
}
// See if this part of the buffer contains the BOM
if (messageContext.bom == 0)
{
messageContext.bom = getBOM(readBuffer);
}
// DEBUG
debug("run", "bom: " + messageContext.bom);
String encoding;
try
{
encoding = getEncoding();
readMsg =
new String(
readBuffer,
0,
readLen,
Utils.getJavaEncoding(encoding));
setEncoding(readMsg);
if (!encoding.equals(getEncoding()))
{
encoding = getEncoding();
readMsg =
new String(
readBuffer,
0,
readLen,
Utils.getJavaEncoding(encoding));
}
}
catch (UnsupportedEncodingException uee)
{
debug("run", "EXCEPTION (3): " + uee.toString());
throw new RuntimeException(uee.toString());
}
// Set encoding in the message context
messageContext.encoding = encoding;
// DEBUG
debug("run", "encoding: " + messageContext.encoding);
// Process message
messageContext = processMessage(readLen, readMsg, messageContext);
}
// If message is complete, then log it and reset buffer
if ((isMessageComplete(messageContext))
|| ((readLen == -1) && (messageContext.messageBuffer.length() > 0)))
{
// Log message
logMessage(messageContext);
// Set message complete
messageComplete = true;
}
}
catch (InterruptedIOException ie)
{
// Set read timeout flag
readTimedOut = true;
debug("run", "InterruptedIOException: " + ie.toString());
// If the read is not done, then shutdown
if (pairedSocketHandler != null
&& pairedSocketHandler.isReadWaiting()
&& pairedSocketHandler.isReadTimedOut())
{
// DEBUG:
debug("run", "read timed out on both sockets");
// If there is data in the message buffer and it is complete, then log it
if ((isMessageComplete(messageContext))
|| (messageContext.messageBuffer.length() > 0))
{
// Log message
logMessage(messageContext);
}
// Set message complete
connectionActive = false;
messageComplete = true;
}
}
catch (Exception e2)
{
// DEBUG:
debug(
"run",
"EXCEPTION (2): "
+ e2.toString()
+ "\n"
+ Utils.getExceptionDetails(e2));
//e2.printStackTrace();
// If there is data in the message buffer and it is complete, then log it
if ((isMessageComplete(messageContext))
|| (messageContext.messageBuffer.length() > 0))
{
// Log message
logMessage(messageContext);
}
// Set message complete
connectionActive = false;
messageComplete = true;
}
}
}
}
catch (Exception e)
{
// DEBUG:
debug(
"run",
"EXCEPTION (1): "
+ e.getMessage()
+ "\n"
+ Utils.getExceptionDetails(e));
//e.printStackTrace();
}
catch (Error err)
{
// DEBUG:
debug("run", "ERROR: " + err.getMessage());
//err.printStackTrace();
}
finally
{
shutdown();
socketConnection.wakeUp();
}
}
/**
* Process the message.
*/
private MessageContext processMessage(
int readLen,
String readMsg,
MessageContext inMessageContext)
throws WSIException
{
boolean continueRead = false;
// Initialize message context
MessageContext messageContext = inMessageContext;
// Get message buffer and chunked data from message context
StringBuffer messageBuffer = messageContext.messageBuffer;
ChunkedData chunkedData = messageContext.chunkedData;
// If all we received was the header with 100 continue, then ignore it
if ((readMsg.toUpperCase().indexOf(HTTP_100_CONTINUE) != -1)
&& (readLen >= 25))
{
// DEBUG:
debug("processMessage", "Ignore HTTP 100 Continue.");
// Find the end of the HTTP 100 message
int index = Utils.getFirstCRLFCRLF(readMsg);
// If there is only the HTTP 100 message, then just ignore it
if (index == readMsg.length())
continueRead = true;
// Otherwise remove the HTTP 100 message and continue
else
readMsg = readMsg.substring(index);
}
// ADD: What if a bypassed message contains another message after it?
if (!continueRead && bypassMessage(readMsg))
{
// DEBUG:
debug(
"processMessage",
"Do not log message as defined in the monitor spec, but it will be sent.");
continueRead = true;
}
if (!continueRead)
{
int index = 0;
// If there is chunked data, then get the length
if ((readMsg.toUpperCase().indexOf(CHUNKED) != -1)
|| (readMsg.toUpperCase().indexOf(CHUNKED_WITH_QUOTES) != -1))
{
// DEBUG:
debug("processMessage", "Processing chunked data...");
// Get the location of the first CFLF
if ((index = readMsg.indexOf(CRLF + CRLF)) == -1)
{
throw new WSIException("Could not locate end of HTTP header.");
}
// Include the CRLF+CRLF in the index
index += 4;
// DEBUG:
debug(
"processMessage",
"Add header before decoding chunked data: ["
+ readMsg.substring(0, index)
+ "]");
// Add HTTP header to buffer
messageBuffer.append(readMsg.substring(0, index));
// If there is no more data (i.e. header only), then just indicate that there is more chunked data
if (readMsg.length() == index)
{
chunkedData = new ChunkedData(this, true);
// DEBUG:
debug(
"processMessage",
"There is chunk data, but none in this part of the message.");
}
// Determine if the remainder of the data is complete (i.e. ends with [0][CRLF][Optional Footer][CRLF])
else
{
// Create chunked data object
chunkedData = new ChunkedData(this, readMsg.substring(index));
if (!chunkedData.isMoreChunkedData())
{
chunkedData.decodeAndAddDataToBuffer(messageBuffer);
}
}
}
else if (chunkedData != null && chunkedData.isMoreChunkedData())
{
// DEBUG:
debug("processMessage", "Processing MORE chunked data...");
// Add data
chunkedData.addData(readMsg);
// Decode data
if (!chunkedData.isMoreChunkedData())
{
chunkedData.decodeAndAddDataToBuffer(messageBuffer);
}
}
// Else just append the data to the buffer
else
{
// DEBUG:
debug(
"processMessage",
"Add data to message entry buffer: [" + readMsg + "]");
messageBuffer.append(readMsg);
}
}
// Set updated message buffer and chunked data in message context
messageContext.messageBuffer = messageBuffer;
messageContext.chunkedData = chunkedData;
// Return message context
return messageContext;
}
/**
* Shutdown input socket and close input stream.
* @param inSocket in socket.
* @param inputStream input stream.
*/
protected void stopInput(Socket inSocket, InputStream inputStream)
{
try
{
// If there is a input socket, then shutdown the input
if (inSocket != null)
{
inSocket.shutdownInput();
}
// If there is an input stream then close it
if (inputStream != null)
{
inputStream.close();
}
}
catch (Exception e)
{
// Ignore since we are stopping everything
}
inputStream = null;
}
/**
* Shutdown output socket and close output stream.
* @param outSocket out socket.
* @param outputStream output stream.
*/
protected void stopOutput(Socket outSocket, OutputStream outputStream)
{
try
{
// If there is an output stream, then flush it
if (outputStream != null)
{
outputStream.flush();
}
// If there is a input socket, then shutdown the input
if (outSocket != null)
{
outSocket.shutdownOutput();
}
// If there is an output stream then close it
if (outputStream != null)
{
outputStream.close();
}
}
catch (Exception e)
{
// Ignore since we are stopping everything
}
outputStream = null;
}
/**
* Shutdown handler.
*/
public void shutdown()
{
// Stop both the input and output
stopInput(this.inSocket, this.inputStream);
stopOutput(this.outSocket, this.outputStream);
}
/**
* Display debug messages.
*/
void debug(String method, String message)
{
debug("SocketHandler", method, message);
}
/**
* Display debug messages.
*/
void debug(String className, String method, String message)
{
if (verbose)
print(className, method, message);
}
/**
* Display messages.
*/
void print(String className, String method, String message)
{
System.out.println(
"["
+ Thread.currentThread().getName()
+ "] ["
+ className
+ "."
+ method
+ "] ["
+ this.connectionType
+ "] "
+ message);
}
/**
* Write data.
*/
private void write(
OutputStream outputStream,
byte[] buffer,
int start,
int length)
throws IOException
{
if (outputStream == null)
{
// DEBUG:
debug("write", "Could not write buffer because output stream is null.");
}
else
{
// DEBUG:
debug("write", "buffer: [" + new String(buffer, start, length) + "]");
outputStream.write(buffer, start, length);
}
}
/**
* Check if message is complete.
*
* @param messageContext
*/
private boolean isMessageComplete(MessageContext messageContext)
throws WSIException
{
int index, index2, contentLen;
boolean messageComplete = false;
boolean moreChunkedData = messageContext.chunkedData.isMoreChunkedData();
String message = messageContext.messageBuffer.toString();
// Find the first CRLF + CRLF which marks the end of the HTTP header
String httpHeader;
index = Utils.getFirstCRLFCRLF(message);
if (index == -1)
httpHeader = message;
else
httpHeader = message.substring(0, index);
// If chunked data, then complete only if there is no more data
if (((httpHeader.toUpperCase().indexOf(CHUNKED) != -1)
|| (httpHeader.toUpperCase().indexOf(CHUNKED_WITH_QUOTES) != -1))
&& (!moreChunkedData))
{
debug(
"isMessageComplete",
"HTTP header indicates chunked data and there is no more chunked data");
messageComplete = true;
}
// Check for content length
else if ((index = httpHeader.toUpperCase().indexOf(CONTENT_LENGTH)) == -1)
{
debug("isMessageComplete", "HTTP header does not contain content length");
// Should not have complete POST header without content length
if (httpHeader.startsWith("POST"))
{
if (httpHeader.endsWith(CRLF + CRLF))
{
throw new WSIException("Could not locate content-length in HTTP POST header.");
}
messageComplete = false;
}
// This could be a GET, so see if the the complete header has been received
else if (
httpHeader.startsWith("GET")
&& (message.length() == httpHeader.length()
&& message.endsWith(CRLF + CRLF)))
{
messageComplete = true;
}
else
{
messageComplete = false;
}
}
// If there is content length, then see if the entire message has been received
else if ((index = httpHeader.toUpperCase().indexOf(CONTENT_LENGTH)) != -1)
{
// Find end of content length value
index2 = httpHeader.indexOf(CRLF, index);
debug("isMessageComplete", "CRLF: " + Utils.toHexString(CRLF));
debug(
"isMessageComplete",
"httpHeader/index: " + Utils.toHexString(httpHeader.substring(index)));
// Get content length
contentLen =
Integer
.decode(
httpHeader.substring(index + CONTENT_LENGTH.length() + 1, index2))
.intValue();
// DEBUG:
debug("isMessageComplete", "contentLen: " + contentLen);
// Find the first CRLF + CRLF which marks the end of the HTTP header
index = Utils.getFirstCRLFCRLF(message);
// DEBUG:
debug(
"isMessageComplete",
"actual received message length: " + (message.length() - (index)));
// If content length is equal to actual message content length, then message is complete
if (contentLen == message.length() - index)
{
messageComplete = true;
// DEBUG:
debug(
"isMessageComplete",
"contentLen = actual received message length.");
}
}
// Message is not complete
else
{
messageComplete = false;
}
// DEBUG:
debug("isMessageComplete", "messageComplete: " + messageComplete);
return messageComplete;
}
/**
* Log message.
*
* @param messageBuffer
*/
private void logMessage(MessageContext messageContext) throws WSIException
{
// Determine sender and receiver host/port
String senderHostAndPort, receiverHostAndPort;
// Request
if (connectionType.equals(MessageEntry.TYPE_REQUEST))
{
senderHostAndPort =
inSocket.getInetAddress().getHostAddress() + ":" + inSocket.getPort();
receiverHostAndPort = targetHost + ":" + outSocket.getPort();
}
// Response
else
{
senderHostAndPort = targetHost + ":" + inSocket.getPort();
receiverHostAndPort =
outSocket.getInetAddress().getHostAddress() + ":" + outSocket.getPort();
}
// Create message entry
this.socketConnection.logMessage(
conversationID,
this.connectionType,
messageContext.timestamp,
senderHostAndPort,
receiverHostAndPort,
messageContext.messageBuffer,
messageContext.bom,
messageContext.encoding);
}
/**
* Check for HTTP messages that should not be logged.
*/
private boolean bypassMessage(String message)
{
boolean bypass = false;
if ((message.startsWith("CONNECT"))
|| (message.startsWith("TRACE"))
|| (message.startsWith("DELETE"))
|| (message.startsWith("OPTIONS"))
|| (message.startsWith("HEAD")))
{
bypass = true;
}
return bypass;
}
// I18N: 2003.02.26 modified by K.Nakagome@BeaconIT
/**
* Set Encoding Parameters
* @param messageFragment String of a HTTP message fragment.
* @author K.Nakagome@BeaconIT Japan SIG
*/
private void setEncoding(String messageFragment)
{
if (mimeCharset == null || mimeCharset.length() == 0)
{
mimeCharset = Utils.getHTTPCharset(messageFragment);
}
if (xmlEncoding == null || xmlEncoding.length() == 0)
{
xmlEncoding = Utils.getXMLEncoding(messageFragment);
}
return;
}
// I18N: 2003.02.26 modified by K.Nakagome@BeaconIT
/**
* Get Encoding Parameter
* @return Character encoding of HTTP message.
* @author K.Nakagome@BeaconIT Japan SIG
*/
private String getEncoding()
{
String encoding = WSIConstants.DEFAULT_XML_ENCODING;
if (mimeCharset != null && mimeCharset.length() > 0)
{
encoding = mimeCharset;
}
if (xmlEncoding != null && xmlEncoding.length() > 0)
{
encoding = xmlEncoding;
}
return encoding;
}
/**
* Get the Byte Order Mark from the message (if there is one).
*/
private int getBOM(byte[] message)
{
int bom = 0;
byte FF = (byte) 0xFF;
byte FE = (byte) 0xFE;
byte EF = (byte) 0xEF;
byte BB = (byte) 0xBB;
byte BF = (byte) 0xBF;
// Search through the byte array for CRLF+CRLF. This will mark the end of the header.
int i = Utils.getFirstCRLFCRLF(message, 0);
if (i != -1)
{
// DEBUG:
debug(
"getBOM",
"message[i]: "
+ message[i]
+ ", message[i+1]: "
+ message[i+1]);
// Check for UTF-8 BOM
if (((i + 2) < message.length)
&& message[i] == EF
&& message[i+1] == BB
&& message[i+2] == BF)
{
bom = 0xEFBBBF;
}
// Check for UTF-16 big-endian BOM
else if (
((i+1) < message.length)
&& message[i] == FE
&& message[i + 1] == FF)
{
bom = 0xFEFF;
}
// Check for UTF-16 little-endian BOM
else if (
((i+1) < message.length)
&& message[i] == FF
&& message[i+1] == FE)
{
bom = 0xFFFE;
}
// ADD: Do we need to check for other BOMs
}
return bom;
}
/**
* Determine if the read is still waiting for data.
*/
boolean isReadWaiting()
{
boolean readWaiting = false;
try
{
// DEBUG:
debug(
"isReadWaiting",
"inSocket.getInputStream().available(): "
+ inSocket.getInputStream().available());
if (inSocket.getInputStream().available() == 0)
{
readWaiting = true;
}
}
catch (IOException ioe)
{
}
return readWaiting;
}
/**
* Get read timed out flag.
*/
boolean isReadTimedOut()
{
return this.readTimedOut;
}
/**
* Message context contains information about the message that is being processed.
*/
class MessageContext
{
StringBuffer messageBuffer;
ChunkedData chunkedData;
String timestamp;
int bom;
String encoding;
/**
* Create new message context.
*/
MessageContext()
{
messageBuffer = new StringBuffer();
chunkedData = new ChunkedData();
timestamp = null;
bom = 0;
encoding = WSIConstants.DEFAULT_XML_ENCODING;
}
}
}