/*
* Sun Public License
*
* The contents of this file are subject to the Sun Public License Version
* 1.0 (the "License"). You may not use this file except in compliance with
* the License. A copy of the License is available at http://www.sun.com/
*
* The Original Code is the SLAMD Distributed Load Generation Engine.
* The Initial Developer of the Original Code is Neil A. Wilson.
* Portions created by Neil A. Wilson are Copyright (C) 2004-2010.
* Some preexisting portions Copyright (C) 2002-2006 Sun Microsystems, Inc.
* All Rights Reserved.
*
* Contributor(s): Neil A. Wilson
*/
package com.slamd.tools.ldapdecoder;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import javax.net.ssl.SSLServerSocketFactory;
import com.slamd.asn1.ASN1DecodeResult;
import com.slamd.asn1.ASN1Element;
import com.slamd.tools.ldapdecoder.protocol.LDAPMessage;
import com.slamd.tools.ldapdecoder.snoop.EthernetHeader;
import com.slamd.tools.ldapdecoder.snoop.IPv4Header;
import com.slamd.tools.ldapdecoder.snoop.SnoopDecoder;
import com.slamd.tools.ldapdecoder.snoop.SnoopException;
import com.slamd.tools.ldapdecoder.snoop.SnoopPacketRecord;
import com.slamd.tools.ldapdecoder.snoop.TCPDumpDecoder;
import com.slamd.tools.ldapdecoder.snoop.TCPDumpPacketRecord;
import com.slamd.tools.ldapdecoder.snoop.TCPHeader;
/**
* This program defines a utility that can be used to decode LDAP traffic and
* display it in human-readable form. It acts as a very simple proxy server,
* in which LDAP clients communicate with this program which decodes the request
* and displays it to the end user, as well as forwarding the request on to the
* directory server. It also accepts and decodes the response from the server
* as well as forwarding it back to the client.
*
*
* @author Neil A. Wilson
*/
public class LDAPDecoder
extends Thread
{
// Indicates whether the raw bytes of the LDAP communication should be
// displayed in addition to the decoded human-readable output.
protected boolean displayRawBytes = false;
// Indicates whether to exclude responses from the server in the generated
// script.
private boolean excludeResponses = false;
// Indicates whether communication from multiple clients should be written to
// separate files.
private boolean separateFilePerConnection = false;
// Indicates whether a specific server address was specified on the command
// line (necessary for working in offline mode).
private boolean serverAddressSpecified = false;
// Indicates whether to use SSL when communicating with LDAP clients.
private boolean useSSLForClients = false;
// Indicates whether to use SSL when communicating with the directory server.
protected boolean useSSLForServer = false;
// Indicates whether to operate in verbose mode.
protected boolean verboseMode = false;
// Indicates whether to write the output as a SLAMD script.
protected boolean writeJobScript = false;
// The map of partial data read for connections.
private HashMap<ConnectionIdentifier,byte[]> partialDataMap =
new HashMap<ConnectionIdentifier,byte[]>();
// The port on which to listen for LDAP requests from clients.
private int listenPort = -1;
// The port on which the directory server is listening for LDAP requests.
protected int serverPort = 389;
// The common output writer that will be used for logging all communication
// unless a separate output file should be created per client.
private PrintStream commonOutputWriter = System.out;
// The output writer that will be used for writing a SLAMD job script based on
// the LDAP communication.
protected PrintStream scriptWriter = null;
// The input file that contains the network packet capture data to use rather
// that obtaining it while operating in proxy mode.
private String inputFile = null;
// The address on which to listen for LDAP requests from clients.
private String listenAddress = "0.0.0.0";
// The file to which the decoded LDAP communication is to be written.
private String outputFile = null;
// The file to which the SLAMD job script should be written.
private String scriptFile = null;
// The address on which the directory server is listening for LDAP requests.
protected String serverAddress = "127.0.0.1";
/**
* Invokes the constructor using the provided command-line arguments and
* creates a new instance of this LDAP decoder.
*
* @param args The command-line arguments provided to this program.
*/
public static void main(String[] args)
{
LDAPDecoder decoder = new LDAPDecoder(args);
if (decoder.offlineMode())
{
decoder.runInOfflineMode();
}
else
{
decoder.setName("LDAPDecoder Main Thread");
decoder.start();
}
}
/**
* Creates a new instance of this LDAP decoder based on the information
* contained in the provided command-line arguments.
*
* @param args The command-line arguments provided to this program.
*/
public LDAPDecoder(String[] args)
{
// Iterate through the arguments and assign their values to instance
// variables.
for (int i=0; i < args.length; i++)
{
if (args[i].equals("-h"))
{
serverAddress = args[++i];
serverAddressSpecified = true;
}
else if (args[i].equals("-p"))
{
serverPort = Integer.parseInt(args[++i]);
}
else if (args[i].equals("-l"))
{
listenAddress = args[++i];
}
else if (args[i].equals("-L"))
{
listenPort = Integer.parseInt(args[++i]);
}
else if (args[i].equals("-i"))
{
inputFile = args[++i];
}
else if (args[i].equals("-f"))
{
outputFile = args[++i];
}
else if (args[i].equals("-F"))
{
writeJobScript = true;
scriptFile = args[++i];
}
else if (args[i].equals("-x"))
{
excludeResponses = true;
}
else if (args[i].equals("-m"))
{
separateFilePerConnection = true;
}
else if (args[i].equals("-s"))
{
useSSLForServer = true;
}
else if (args[i].equals("-S"))
{
useSSLForClients = true;
}
else if (args[i].equals("-b"))
{
displayRawBytes = true;
}
else if (args[i].equals("-v"))
{
verboseMode = true;
}
else if (args[i].equals("-H"))
{
displayUsage();
System.exit(0);
}
else
{
System.err.println("Unrecognized argument \"" + args[i] + '"');
displayUsage();
System.exit(1);
}
}
// Make sure that either an input file or a valid listen port was specified.
if ((inputFile == null) || (inputFile.length() == 0))
{
if ((listenPort < 1) || (listenPort > 65535))
{
System.err.println("Invalid or unspecified listen port (use -L)");
displayUsage();
System.exit(1);
}
}
// Make sure that there were not any conflicting arguments.
if (separateFilePerConnection && (inputFile != null))
{
System.err.println("Only a single output file may be specified when " +
"parsing an input file rather than running in proxy " +
"mode");
System.exit(1);
}
if (separateFilePerConnection &&
((outputFile == null) || (outputFile.length() == 0) ||
outputFile.equals("-")))
{
System.err.println("An output file must be specified when the -m " +
"option is used");
displayUsage();
System.exit(1);
}
// If a single output file is to be used, create it.
if ((outputFile != null) && (outputFile.length() > 0) &&
(! outputFile.equals("-")) && (! separateFilePerConnection))
{
try
{
commonOutputWriter = new PrintStream(new FileOutputStream(outputFile,
true));
}
catch (IOException ioe)
{
System.err.println("ERROR: Unable to open output file \"" +
outputFile + "\" for writing");
ioe.printStackTrace();
System.exit(1);
}
}
// If a script file is to be written, then create the file and write the
// header to it.
if (writeJobScript)
{
try
{
scriptWriter = new PrintStream(new FileOutputStream(scriptFile, false));
}
catch (IOException ioe)
{
System.err.println("ERROR: Unable to open script file \"" +
scriptFile + "\" for writing");
ioe.printStackTrace();
System.exit(1);
}
scriptWriter.println("# This script was dynamically generated by the " +
"the SLAMD LDAPDecoder tool.");
scriptWriter.println("# Generation Date: " + new Date());
scriptWriter.println();
scriptWriter.println();
scriptWriter.println("# Make the LDAP data types available for use.");
scriptWriter.println("use com.slamd.scripting.ldap." +
"LDAPAttributeVariable;");
scriptWriter.println("use com.slamd.scripting.ldap." +
"LDAPConnectionVariable;");
scriptWriter.println("use com.slamd.scripting.ldap." +
"LDAPEntryVariable;");
scriptWriter.println("use com.slamd.scripting.ldap." +
"LDAPModificationVariable;");
scriptWriter.println("use com.slamd.scripting.ldap." +
"LDAPModificationSetVariable;");
scriptWriter.println();
scriptWriter.println();
scriptWriter.println("# Define the variables that we will use.");
scriptWriter.println("variable boolean useSSL;");
scriptWriter.println("variable int resultCode;");
scriptWriter.println("variable int port;");
scriptWriter.println("variable LDAPConnection conn;");
scriptWriter.println("variable LDAPEntry entry;");
scriptWriter.println("variable LDAPModification mod;");
scriptWriter.println("variable LDAPModificationSet modSet;");
scriptWriter.println("variable string bindDN;");
scriptWriter.println("variable string bindPW;");
scriptWriter.println("variable string host;");
scriptWriter.println("variable string message;");
scriptWriter.println("variable StringArray searchAttrs;");
scriptWriter.println();
scriptWriter.println();
scriptWriter.println("# Read the values of all the configuration " +
"arguments.");
scriptWriter.println("host = script.getScriptArgument(\"host\", \"" +
serverAddress + "\");");
scriptWriter.println("port = script.getScriptIntArgument(\"port\", " +
serverPort + ");");
scriptWriter.println("useSSL = script.getScriptBooleanArgument(" +
"\"useSSL\", " +
(useSSLForServer ? "true" : "false") + ");");
scriptWriter.println("bindDN = script.getScriptArgument(\"bindDN\", " +
"\"\");");
scriptWriter.println("bindPW = script.getScriptArgument(\"bindPW\", " +
"\"\");");
scriptWriter.println();
scriptWriter.println();
scriptWriter.println("# Indicate that the connection should collect " +
"and report statistics.");
scriptWriter.println("conn.enableAttemptedOperationCounters();");
scriptWriter.println("conn.enableSuccessfulOperationCounters();");
scriptWriter.println("conn.enableFailedOperationCounters();");
scriptWriter.println("conn.enableOperationTimers();");
scriptWriter.println();
scriptWriter.println();
scriptWriter.println("# Establish the connection that will be used for " +
"all the work. If the");
scriptWriter.println("# connection attempt fails, then exit with an " +
"error.");
scriptWriter.println("resultCode = conn.connect(host, port, bindDN, " +
"bindPW, 3, useSSL);");
scriptWriter.println("if resultCode.notEqual(conn.success())");
scriptWriter.println("begin");
scriptWriter.println(" message = \"Unable to connect. Result code " +
"was: \";");
scriptWriter.println(" message = " +
"message.append(resultCode.toString());");
scriptWriter.println(" script.logMessage(message);");
scriptWriter.println(" script.exitWithError();");
scriptWriter.println("end;");
scriptWriter.println();
scriptWriter.println();
scriptWriter.flush();
Runtime runtime = Runtime.getRuntime();
runtime.addShutdownHook(new LDAPDecoderShutdownHook(this));
}
}
/**
* Operates in a loop, accepting new client connections and handing them off
* to worker threads to actually handle decoding the communication between
* them.
*/
public void run()
{
// Create the server socket to accept connections from clients.
ServerSocket serverSocket;
if (useSSLForClients)
{
try
{
InetAddress listenInetAddress = InetAddress.getByName(listenAddress);
SSLServerSocketFactory socketFactory =
(SSLServerSocketFactory) SSLServerSocketFactory.getDefault();
serverSocket = socketFactory.createServerSocket(listenPort, 128,
listenInetAddress);
}
catch (Exception e)
{
System.err.println("Unable to create SSL listener to accept client " +
"connections:");
e.printStackTrace();
return;
}
}
else
{
try
{
InetAddress listenInetAddress = InetAddress.getByName(listenAddress);
serverSocket = new ServerSocket(listenPort, 128, listenInetAddress);
}
catch (Exception e)
{
System.err.println("Unable to create listener to accept client " +
"connections:");
e.printStackTrace();
return;
}
}
// Print out a message so the user knows the decoder is ready.
System.err.println("Listening on " + listenAddress + ':' + listenPort +
" for client connections");
// Operate in a loop accepting connections and handing them off to be
// handled by other threads.
boolean failedLastTime = false;
while (true)
{
try
{
// Accept a new connection from the client.
Socket clientSocket = serverSocket.accept();
// Prepare the output writer to use for the connection.
PrintStream outputWriter;
if (separateFilePerConnection)
{
String fileName = outputFile + '.' +
clientSocket.getInetAddress().getHostAddress() +
'.' + clientSocket.getPort();
outputWriter = new PrintStream(new FileOutputStream(fileName, true));
}
else
{
outputWriter = commonOutputWriter;
}
// Create a new client connection object and use it to handle the
// communication between the client and the server.
new LDAPClientConnection(this, clientSocket, outputWriter);
failedLastTime = false;
}
catch (Exception e)
{
if (failedLastTime)
{
System.err.println("Disabling the listener due to consecutive " +
"failures while accepting connections");
return;
}
else
{
failedLastTime = true;
}
}
}
}
/**
* Indicates whether the LDAPDecoder is configured to operate in offline mode.
*
* @return <CODE>true</CODE> if it is configured to operate in offline mode,
* or <CODE>false</CODE> if it is to operate in proxy mode.
*/
public boolean offlineMode()
{
return (inputFile != null);
}
/**
* Runs the LDAP decoder in offline mode, reading the data from the specified
* input file.
*/
public void runInOfflineMode()
{
// If a server address was specified, then resolve it to an IP address if
// necessary.
if (serverAddressSpecified)
{
try
{
serverAddress = InetAddress.getByName(serverAddress).getHostAddress();
}
catch (Exception e)
{
System.err.println("ERROR: Unable to resolve \"" + serverAddress +
"\" -- " + e);
if (verboseMode)
{
e.printStackTrace();
}
System.exit(1);
}
}
// Open an input stream to the specified file.
FileInputStream inputStream = null;
try
{
inputStream = new FileInputStream(inputFile);
}
catch (IOException ioe)
{
System.err.println("ERROR: Unable to open the input file \"" +
inputFile + "\" -- " + ioe);
if (verboseMode)
{
ioe.printStackTrace();
}
System.exit(1);
}
// Peek at the beginning of the file to determine whether it is a snoop or
// libpcap dump.
try
{
byte initialByte = (byte) (inputStream.read() & 0x000000FF);
if (inputStream.markSupported())
{
inputStream.reset();
}
else
{
inputStream.close();
inputStream = new FileInputStream(inputFile);
}
if (initialByte == SnoopDecoder.SNOOP_HEADER_BYTES[0])
{
handleSnoopCapture(inputStream);
}
else if ((initialByte == (byte) 0xa1) || (initialByte == (byte) 0xd4))
{
handleTCPDumpCapture(inputStream);
}
else
{
System.err.println("Unable to determine the capture file format");
System.exit(1);
}
}
catch (IOException ioe)
{
System.err.println("ERROR: Unable to read first byte from the capture " +
"file -- " + ioe);
if (verboseMode)
{
ioe.printStackTrace();
}
System.exit(1);
}
}
/**
* Handles the work of parsing and interpreting a snoop capture.
*
* @param inputStream The input stream from which to read the snoop
* capture.
*/
private void handleSnoopCapture(InputStream inputStream)
{
// Create the decoder that we will use to do the real work.
SnoopDecoder snoopDecoder = null;
try
{
snoopDecoder = new SnoopDecoder(inputStream);
if (verboseMode)
{
System.err.println("Initialized the snoop decoder");
}
}
catch (SnoopException se)
{
System.err.println("ERROR: " + se.getMessage());
if (verboseMode)
{
se.printStackTrace();
}
System.exit(1);
}
catch (Exception e)
{
System.err.println("ERROR: Unable to initialize the snoop decoder -- " +
e);
if (verboseMode)
{
e.printStackTrace();
}
System.exit(1);
}
// Make sure that the data captured was from an Ethernet datalink type. If
// not, then we don't know how to handle it.
if (snoopDecoder.getDataLinkType() != SnoopDecoder.DATA_LINK_TYPE_ETHERNET)
{
System.err.println("ERROR: Unsupported datalink type for snoop " +
"capture");
System.err.println("Only Ethernet captures are currently supported");
System.exit(1);
}
// Iterate through the snoop packet records and evaluate them to decide
// whether they contain LDAP traffic.
boolean errorOccurred = false;
int packetNumber = 0;
int cumulativeDrops = 0;
int ldapPackets = 0;
int errorPackets = 0;
int skippedPackets = 0;
SimpleDateFormat dateFormat =
new SimpleDateFormat("[dd/MMM/yyyy:HH:mm:ss.SSS Z]");
while (true)
{
SnoopPacketRecord packetRecord = null;
try
{
packetRecord = snoopDecoder.nextPacketRecord();
if (packetRecord == null)
{
if (verboseMode)
{
System.err.println("Reached the end of the capture file");
}
break;
}
packetNumber++;
}
catch (SnoopException se)
{
System.err.println("ERROR: Unable to decode snoop packet record -- " +
se.getMessage());
if (verboseMode)
{
se.printStackTrace();
}
errorOccurred = true;
break;
}
catch (IOException ioe)
{
System.err.println("ERROR: Unable to read next packet record from " +
"input file -- " + ioe);
if (verboseMode)
{
ioe.printStackTrace();
}
errorOccurred = true;
break;
}
// See if any packets may have been dropped and if so print a warning.
if (packetRecord.getCumulativeDrops() > cumulativeDrops)
{
int numDrops = packetRecord.getCumulativeDrops() - cumulativeDrops;
cumulativeDrops += numDrops;
if (verboseMode)
{
System.err.println("WARNING: Detected " + numDrops +
" dropped packet(s) between packets " +
(packetNumber-1) + " and " + packetNumber);
}
}
// Determine if the packet record was truncated. If so, then we can't do
// anything with it.
if (packetRecord.isTruncated())
{
if (verboseMode)
{
System.err.println("WARNING: Skipping truncated packet " +
packetNumber);
}
continue;
}
// Get the timestamp for this packet
long captureTime = (packetRecord.getTimestampSeconds() * 1000) +
(packetRecord.getTimestampMicroseconds() / 1000);
String packetTimestamp = dateFormat.format(new Date(captureTime));
// Get the data for the packet and decode the Ethernet header.
byte[] packetData = packetRecord.getPacketData();
int resultCode = decodePacketData(packetData, packetNumber,
packetTimestamp);
switch (resultCode)
{
case -1:
errorPackets++;
break;
case 0:
skippedPackets++;
break;
case 1:
ldapPackets++;
break;
}
}
// Close the input and output streams.
try
{
inputStream.close();
} catch (Exception e) {}
try
{
commonOutputWriter.flush();
commonOutputWriter.close();
} catch (Exception e) {}
// Print a summary of the results.
System.out.println("Processed " + packetNumber + " total packets");
System.out.println("Processed " + ldapPackets + " LDAP packets");
System.out.println("Skipped " + skippedPackets + " non-LDAP packets");
System.out.println("Encountered " + errorPackets + " decoding errors");
// Exit gracefully or with an error, depending on whether any fatal decoding
// errors were encountered.
if (errorOccurred)
{
System.exit(1);
}
else
{
System.exit(0);
}
}
/**
* Handles the work of parsing and interpreting a tcpdump capture.
*
* @param inputStream The input stream from which to read the tcpdump
* capture.
*/
private void handleTCPDumpCapture(InputStream inputStream)
{
// Create the decoder that we will use to do the real work.
TCPDumpDecoder tcpDumpDecoder = null;
try
{
tcpDumpDecoder = new TCPDumpDecoder(inputStream);
if (verboseMode)
{
System.err.println("Initialized the tcpdump decoder");
}
}
catch (SnoopException se)
{
System.err.println("ERROR: " + se.getMessage());
if (verboseMode)
{
se.printStackTrace();
}
System.exit(1);
}
catch (Exception e)
{
System.err.println("ERROR: Unable to initialize the tcpdump decoder " +
"-- " + e);
if (verboseMode)
{
e.printStackTrace();
}
System.exit(1);
}
// Make sure that the data captured was from an Ethernet datalink type. If
// not, then we don't know how to handle it.
if (tcpDumpDecoder.getDataLinkType() !=
TCPDumpDecoder.DATA_LINK_TYPE_ETHERNET)
{
System.err.println("ERROR: Unsupported datalink type for tcpdump " +
"capture");
System.err.println("Only Ethernet captures are currently supported");
System.exit(1);
}
// Iterate through the snoop packet records and evaluate them to decide
// whether they contain LDAP traffic.
boolean errorOccurred = false;
int packetNumber = 0;
int ldapPackets = 0;
int errorPackets = 0;
int skippedPackets = 0;
SimpleDateFormat dateFormat =
new SimpleDateFormat("[dd/MMM/yyyy:HH:mm:ss.SSS Z]");
while (true)
{
TCPDumpPacketRecord packetRecord = null;
try
{
packetRecord = tcpDumpDecoder.nextPacketRecord();
if (packetRecord == null)
{
if (verboseMode)
{
System.err.println("Reached the end of the capture file");
}
break;
}
packetNumber++;
}
catch (SnoopException se)
{
System.err.println("ERROR: Unable to decode snoop packet record -- " +
se.getMessage());
if (verboseMode)
{
se.printStackTrace();
}
errorOccurred = true;
break;
}
catch (IOException ioe)
{
System.err.println("ERROR: Unable to read next packet record from " +
"input file -- " + ioe);
if (verboseMode)
{
ioe.printStackTrace();
}
errorOccurred = true;
break;
}
// Determine if the packet record was truncated. If so, then we can't do
// anything with it.
if (packetRecord.isTruncated())
{
if (verboseMode)
{
System.err.println("WARNING: Skipping truncated packet " +
packetNumber);
}
continue;
}
// Get the timestamp for this packet
long captureTime = (packetRecord.getTimestampSeconds() * 1000) +
(packetRecord.getTimestampMicroseconds() / 1000);
String packetTimestamp = dateFormat.format(new Date(captureTime));
// Get the data for the packet and decode the Ethernet header.
byte[] packetData = packetRecord.getPacketData();
int resultCode = decodePacketData(packetData, packetNumber,
packetTimestamp);
switch (resultCode)
{
case -1:
errorPackets++;
break;
case 0:
skippedPackets++;
break;
case 1:
ldapPackets++;
break;
}
}
// Close the input and output streams.
try
{
inputStream.close();
} catch (Exception e) {}
try
{
commonOutputWriter.flush();
commonOutputWriter.close();
} catch (Exception e) {}
// Print a summary of the results.
System.out.println("Processed " + packetNumber + " total packets");
System.out.println("Processed " + ldapPackets + " LDAP packets");
System.out.println("Skipped " + skippedPackets + " non-LDAP packets");
System.out.println("Encountered " + errorPackets + " decoding errors");
// Exit gracefully or with an error, depending on whether any fatal decoding
// errors were encountered.
if (errorOccurred)
{
System.exit(1);
}
else
{
System.exit(0);
}
}
/**
* Decodes the actual Ethernet packet as LDAP data.
*
* @param packetData The actual data contained in this packet.
* @param packetNumber The packet number for this packet.
* @param packetTimestamp The time that the packet was captured.
*
* @return A numeric value that indicates the state of the decoding. A value
* of -1 indicates that an error occurred while processing the data.
* A value of 0 indicates that the packet was processed properly but
* did not contain LDAP data. A value of 1 indicates that the packet
* was processed properly and did contain LDAP data.
*/
private int decodePacketData(byte[] packetData, int packetNumber,
String packetTimestamp)
{
EthernetHeader ethernetHeader = null;
try
{
ethernetHeader = EthernetHeader.decodeEthernetHeader(packetData, 0);
}
catch (SnoopException se)
{
System.err.println("ERROR: Unable to decode Ethernet header for " +
"packet " + packetNumber + " -- " + se.getMessage());
if (verboseMode)
{
se.printStackTrace();
}
return -1;
}
// Make sure the Ethernet header contains IP data.
if (ethernetHeader.getEthertype() != IPv4Header.ETHERTYPE_IPV4)
{
if (verboseMode)
{
System.err.println("NOTICE: Skipping packet " + packetNumber +
" because it does not contain IPv4 data");
}
return 0;
}
// Decode the IPv4 header for the packet.
int ipHeaderStart = ethernetHeader.getHeaderLength();
IPv4Header ipHeader = null;
try
{
ipHeader = IPv4Header.decodeIPv4Header(packetData, ipHeaderStart);
}
catch (SnoopException se)
{
System.err.println("ERROR: Unable to decode IPv4 header for " +
"packet " + packetNumber + " -- " + se.getMessage());
if (verboseMode)
{
se.printStackTrace();
}
return -1;
}
// Make sure the IP header contains TCP data.
if (ipHeader.getProtocol() != TCPHeader.IP_PROTOCOL_TCP)
{
if (verboseMode)
{
System.err.println("NOTICE: Skipping packet " + packetNumber +
" because it does not contain TCP data");
}
return 0;
}
// If a server address was specified, then see if this packet has a source
// or destination of that address.
String sourceIP = IPv4Header.intToIPAddress(ipHeader.getSourceIP());
String destIP = IPv4Header.intToIPAddress(ipHeader.getDestinationIP());
if (serverAddressSpecified)
{
if (! (serverAddress.equals(sourceIP) || serverAddress.equals(destIP)))
{
if (verboseMode)
{
System.err.println("NOTICE: Skipping packet " + packetNumber +
" because neither the source nor destination " +
"address matched the provided server address");
}
return 0;
}
}
// Decode the TCP header for the packet.
int tcpHeaderStart = ipHeaderStart + ipHeader.getIPHeaderLength();
TCPHeader tcpHeader = null;
try
{
tcpHeader = TCPHeader.decodeTCPHeader(packetData, tcpHeaderStart);
}
catch (SnoopException se)
{
System.err.println("ERROR: Unable to decode TCP header for " +
"packet " + packetNumber + " -- " + se.getMessage());
if (verboseMode)
{
se.printStackTrace();
}
return -1;
}
// See whether the source or destination port from the TCP header matches
// the server port.
int sourcePort = tcpHeader.getSourcePort();
int destPort = tcpHeader.getDestinationPort();
if (! ((serverPort == sourcePort) || (serverPort == destPort)))
{
if (verboseMode)
{
System.err.println("NOTICE: Skipping packet " + packetNumber +
" because neither the source nor destination " +
"port matched the provided server port");
}
return 0;
}
// At this point, we can be reasonably confident that we have a packet
// going to or from the directory server. Get the data (if any) and try
// to decode it as LDAP.
int dataStart = tcpHeaderStart + tcpHeader.getHeaderLength();
if (dataStart == packetData.length)
{
if (verboseMode)
{
System.err.println("NOTICE: Skipping packet " + packetNumber +
" because it does not contain any TCP data");
}
return 0;
}
// There is what we believe to be LDAP data in this packet, so extract it
// and try to interpret it.
byte[] ldapData = new byte[packetData.length - dataStart];
System.arraycopy(packetData, dataStart, ldapData, 0, ldapData.length);
if (displayRawBytes)
{
commonOutputWriter.println("Raw Data from Client:");
commonOutputWriter.println(LDAPMessage.byteArrayToString(ldapData, 4));
}
// Create a ConnectionIdentifier object and see if we have any partial data
// from an earlier packet on this connection.
ConnectionIdentifier connIdentifier =
new ConnectionIdentifier(sourceIP, sourcePort, destIP, destPort);
byte[] existingData = partialDataMap.remove(connIdentifier);
if (existingData != null)
{
if (verboseMode)
{
System.err.println("NOTE: Combining data read in packet " +
packetNumber + " with data read earlier on the same connection.");
}
byte[] newLDAPData = new byte[ldapData.length + existingData.length];
System.arraycopy(existingData, 0, newLDAPData, 0, existingData.length);
System.arraycopy(ldapData, 0, newLDAPData, existingData.length,
ldapData.length);
ldapData = newLDAPData;
}
if (ldapData[0] != (byte) 0x30)
{
// We will ignore this packet because it doesn't contain any LDAP data.
if (verboseMode)
{
System.err.println("NOTICE: Skipping packet " + packetNumber +
" because it does not start with the expected ASN.1 sequence " +
"BER type.");
}
return -1;
}
ASN1DecodeResult decodeResult;
try
{
decodeResult = ASN1Element.decodePartial(ldapData);
}
catch (Exception e)
{
if (verboseMode)
{
System.err.println("WARNING: Skipping packet " + packetNumber +
" because TCP data could not be decoded as an " +
"ASN.1 element -- " + e);
e.printStackTrace();
}
return -1;
}
boolean fullDecoded = false;
while (true)
{
ASN1Element asn1Element = decodeResult.getDecodedElement();
if (asn1Element == null)
{
if (ldapData.length > (20 * 1024 * 1024))
{
// For safety purposes, we won't attempt to handle any packets
// larger than 20MB.
System.err.println("ERROR: Packet " + packetNumber +
", combined with data from earlier packets, does not contain " +
"a complete element even after more than 100MB of data has " +
"been processed. Aborting the attempt to decode this data.");
return -1;
}
else
{
if (verboseMode)
{
System.err.println("NOTICE: Packet " + packetNumber +
" contains LDAP data, but does not have enough to form a " +
"complete ASN.1 element. Saving the partial data to add " +
"to data from subsequent packets on the connection.");
}
partialDataMap.put(connIdentifier, ldapData);
if (fullDecoded)
{
return 1;
}
else
{
return 0;
}
}
}
fullDecoded = true;
commonOutputWriter.println(packetTimestamp + " Data From " + sourceIP +
':' + sourcePort + " to " + destIP + ':' +
destPort);
LDAPMessage message = null;
try
{
message = LDAPMessage.decode(asn1Element);
}
catch (Exception e)
{
if (verboseMode)
{
System.err.println("WARNING: Skipping packet " + packetNumber +
" because ASN.1 element could not be decoded as " +
"an LDAP message -- " + e);
e.printStackTrace();
}
commonOutputWriter.println("Unable to decode data from client: " +
e.getMessage());
commonOutputWriter.println();
commonOutputWriter.println();
return -1;
}
commonOutputWriter.println("Decoded Data from Client:");
commonOutputWriter.println(message.toString(4));
commonOutputWriter.println();
commonOutputWriter.println();
if (writeJobScript)
{
message.toSLAMDScript(scriptWriter);
}
if (verboseMode)
{
System.err.println("NOTICE: Decoded packet " + packetNumber +
" as an " + message.getProtocolOp().getProtocolOpType() +
" message");
}
byte[] remainingData = decodeResult.getRemainingData();
if (remainingData == null)
{
return 1;
}
try
{
ldapData = remainingData;
decodeResult = ASN1Element.decodePartial(ldapData);
}
catch (Exception e)
{
if (verboseMode)
{
System.err.println("WARNING: Remaining TCP data in packet " +
packetNumber + " could not be decoded as an ASN.1 element -- " +
e);
e.printStackTrace();
}
return -1;
}
}
}
/**
* Indicates whether comments providing information about server responses
* should be excluded from the resulting SLAMD job script.
*
* @return {@code true} if information about server responses should be
* excluded, or {@code false} if not.
*/
public boolean excludeResponses()
{
return excludeResponses;
}
/**
* Displays usage information for this LDAP decoder.
*/
public static void displayUsage()
{
String eol = System.getProperty("line.separator");
System.err.println(
"Usage: java -jar LDAPDecoder.jar {options}" + eol +
" where {options} include:" + eol +
"-h {serverAddress} -- Specifies the address of the directory server" + eol +
" to which client requests should be forwarded" + eol +
"-p {serverPort} -- Specifies the port of the directory server to" + eol +
" which client requests should be forwarded" + eol +
"-l {listenAddress} -- Specifies the address on which the LDAP decoder" + eol +
" should accept connections from LDAP clients" + eol +
"-L {listenPort} -- Specifies the port on which the LDAP decoder" + eol +
" should accept connections from LDAP clients" + eol +
"-i {inputFile} -- Specifies the input file containing the capture" + eol +
" data to use when operating in offline mode" + eol +
"-f {outputFile} -- Specifies the path to the output file to which" + eol +
" decoded communication will be written in " + eol +
" human-readable form" + eol +
"-F {outputFile} -- Specifies the path to the output file to which" + eol +
" decoded communication will be written as a" + eol +
" SLAMD job script" + eol +
"-x -- Indicates that server responses should be" + eol +
" -- excluded from the generated SLAMD job script" + eol +
"-m -- Indicates that a separate file output file" + eol +
" should be used for each client connection" + eol +
"-s -- Indicates that communication with the directory" + eol +
" server should be encrypted using SSL" + eol +
"-S -- Indicates that communication with clients" + eol +
" be encrypted using SSL" + eol +
"-b -- Indicates that the raw bytes of the LDAP" + eol +
" communication should be included in the output" + eol +
"-v -- Enables verbose mode (useful for debugging)" + eol +
"-H -- Displays this usage information"
);
}
}