package echosign.api.proxy; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintStream; import java.net.ServerSocket; import java.net.Socket; import java.text.SimpleDateFormat; import java.util.Date; import javax.net.SocketFactory; import javax.net.ssl.SSLSocketFactory; import org.apache.commons.io.IOUtils; /** * A SoapProxy displays the requests and responses exchanged between the client and server. It can * also save the requests and responses and replay the requests later. * <p> * A SoapProxy maintains two streams, one stream from the client to the server and the other from * the server to the client and prints the data going through the streams. It's done with the help * of the classes <b>BufferedHttpStreamReader</b> and <b>CopyStream</b>. * <p> * When to stop streaming is controlled with the two global states, streaming and requesting. Both * streams will be stopped whenever the streaming state turns into 'stop' (normally when the end * of the stream is reached or an IO exception is thrown), or the requesting state turns into 'not * requesting', which will be done only by the client when there is no more requests to send. */ public class SoapProxy { /** * Signify the stream direction, from the client to server or the server to the client. */ public static enum Direction { CLIENT2SERVER, SERVER2CLIENT; } public static final String FROM_CLIENT_TO_SERVER = "From CLIENT to SERVER:"; public static final String FROM_SERVER_TO_CLIENT = "From SERVER to CLIENT:"; public static final String SOAP_ENVELOPE_START = "<soap:Envelope"; // no ">" here, there are arguments to follow public static final String SOAP_ENVELOPE_END = "</soap:Envelope>"; public static final String PRINTABLE_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss.SSS"; public static final String FILENAME_DATE_FORMAT = "yyyy-MM-dd_HH-mm-ss.SSS"; private static final String USAGE_MSG = "Usage: SoapProxy [-r] [-l] [-f] client_port|filename server_hostname server_port" + "\n-r no prettyprint\n-l logging in a created directory\n-f input from file"; private static boolean prettyPrint = true; private static boolean logging = false; private static boolean inputIsFile = false; private static boolean streaming = true; // false when stopping streaming private static boolean requesting = true; // false when client has no more requests to send private static final int DEFAULT_SOCKET_TIMEOUT_MSEC = 1000; /** * The main method * @param args url, apiKey, and a command must be specified at least */ public static void main(String[] args) { int argStart = parseArguments(args); if (argStart < 0 || args.length - argStart != 3) { System.err.println(USAGE_MSG); return; } if (inputIsFile) { processInputFile(args[argStart], args[argStart + 1], Integer.parseInt(args[argStart + 2])); return; } // create log directory if logging File logDirectory = null; if (logging) { logDirectory = createLogDirectory(); } // The terms "client" and "server" in this program apply to the user's view of the situation // rather than to this program's view, which is the other way around. int clientPort = Integer.parseInt(args[argStart]); String serverHost = args[argStart + 1]; int serverPort = Integer.parseInt(args[argStart + 2]); try { // open a server socket that the client will connect to ServerSocket clientServerSocket = new ServerSocket(clientPort); int logFileNumber = 0; for (;;) { PrintStream clientLoggingStream = null; PrintStream serverLoggingStream = null; if (logging) { clientLoggingStream = createLogFile(logDirectory, String.format("%03d", logFileNumber) + "-client"); serverLoggingStream = createLogFile(logDirectory, String.format("%03d", logFileNumber) + "-server"); logFileNumber++; } proxyOneConnection(clientLoggingStream, serverLoggingStream, serverHost, serverPort, clientServerSocket, prettyPrint); } } catch (Exception e) { e.printStackTrace(); } } private static int parseArguments(String[] args) { int argStart = 0; for (;;) { if (argStart >= args.length) { return -1; } if (args[argStart].equals("-r")) { prettyPrint = false; argStart++; } else if (args[argStart].equals("-l")) { logging = true; argStart++; } else if (args[argStart].equals("-f")) { inputIsFile = true; argStart++; } else { return argStart; } } } /** * This method opens the socket streams for sending and receiving the data and then starts two * threads, one thread for sending the requests from the client to the server, and the other for * receiving the responses from the server and pass them back to the client, while displaying the * requests and responses. */ public static void proxyOneConnection(PrintStream clientLoggingStream, PrintStream serverLoggingStream, String serverHost, int serverPort, ServerSocket clientServerSocket, boolean prettyPrint) throws IOException, InterruptedException { System.out.println("Waiting for client to connect (" + clientServerSocket.getInetAddress() + ":" + clientServerSocket.getLocalPort() + ")..."); // accept socket connection from the client and get input and output streams to that socket Socket clientSocket = clientServerSocket.accept(); clientSocket.setSoTimeout(DEFAULT_SOCKET_TIMEOUT_MSEC); InputStream clientInputStream = clientSocket.getInputStream(); OutputStream clientOutputStream = clientSocket.getOutputStream(); System.out.println("Connectiong to server (" + serverHost + ":" + serverPort + ")..."); // get socket connection to the server and get input and output streams SocketFactory socketFactory = SSLSocketFactory.getDefault(); Socket serverSocket = socketFactory.createSocket(serverHost, serverPort); serverSocket.setSoTimeout(DEFAULT_SOCKET_TIMEOUT_MSEC); InputStream serverInputStream = serverSocket.getInputStream(); OutputStream serverOutputStream = serverSocket.getOutputStream(); // reset the states; start streaming and sending the requests startStreaming(); startRequesting(); // create the threads to read and write Thread serverToClientThread = new Thread(new CopyStream(Direction.SERVER2CLIENT, serverHost, serverInputStream, clientOutputStream, serverLoggingStream, prettyPrint)); Thread clientToServerThread = new Thread(new CopyStream(Direction.CLIENT2SERVER, serverHost, clientInputStream, serverOutputStream, clientLoggingStream, prettyPrint)); // start the threads serverToClientThread.start(); clientToServerThread.start(); // join and wait for the threads clientToServerThread.join(); serverToClientThread.join(); // close the sockets clientSocket.close(); serverSocket.close(); } /** * Reset the streaming state to 'streaming'. */ public static void startStreaming() { streaming = true; } /** * Turns the streaming state to 'stop'. Either thread can call this method to signal the other to * stop. */ public static void stopStreaming() { streaming = false; } /** * Returns true if the streaming state is 'streaming'. */ public static boolean isStreaming() { return streaming; } /** * Reset the requesting state to 'requesting'. */ public static void startRequesting() { requesting = true; } /** * Turns the requesting state to 'not requesting'. Only the client thread should call this method * to signal the server thread to stop. */ public static void stopRequesting() { requesting = false; } /** * Returns true if the requesting state is 'requesting'. */ public static boolean isRequesting() { return requesting; } private static File createLogDirectory() { String logDirName = (new SimpleDateFormat(FILENAME_DATE_FORMAT)).format(new Date()); File file = new File(logDirName); if (!file.mkdir()) { System.err.println("Cannot create log directory " + logDirName); System.exit(1); } return file; } public static PrintStream createLogFile(File logDirectory, String logFileName) throws IOException { File f = new File(logDirectory, logFileName); if (!f.canWrite() && !f.createNewFile()) { System.out.println("Cannot create log file " + logFileName); System.exit(1); } return new PrintStream(new FileOutputStream(f), true, "UTF-8"); } /** * If the input is a file (-f argument) process it here. */ private static void processInputFile(String inputFileName, String serverHost, int serverPort) { try { String inputString = getInputString(inputFileName); if (inputString == null) return; // get server socket and input and output streams // also get logging stream if necessary SocketFactory socketFactory = SSLSocketFactory.getDefault(); Socket serverSocket = socketFactory.createSocket(serverHost, serverPort); serverSocket.setSoTimeout(DEFAULT_SOCKET_TIMEOUT_MSEC); InputStream serverInputStream = serverSocket.getInputStream(); OutputStream serverOutputStream = serverSocket.getOutputStream(); PrintStream serverLoggingStream = logging ? createLogFile(createLogDirectory(), "000-server") : null; // reset the states; start streaming and sending a request startStreaming(); startRequesting(); // run CopyStream to get the server input and output it appropriately Thread serverToClientThread = new Thread(new CopyStream(Direction.SERVER2CLIENT, serverHost, serverInputStream, null, serverLoggingStream, prettyPrint)); serverToClientThread.start(); // write the input to standard out and to the server System.out.println(">> " + (new SimpleDateFormat(PRINTABLE_DATE_FORMAT)).format(new Date()) + " " + FROM_CLIENT_TO_SERVER); System.out.println(inputString); serverOutputStream.write(inputString.getBytes("UTF-8")); // done sending the request stopRequesting(); } catch (Exception e) { e.printStackTrace(); } } private static String getInputString(String inputFileName) throws FileNotFoundException, IOException { // The input file, if we created it with pretty printing, will have an incorrect Content-Length // field. So we read the input file into a string and fix up the field. File inputFile = new File(inputFileName); if (!inputFile.exists() || inputFile.length() == 0) { System.out.println("Input file doesn't exist or is empty."); return null; } // read the input file into a string byte[] inputByteArray = new byte[(int)inputFile.length()]; FileInputStream fileInputStream = new FileInputStream(inputFile); int n; try { n = fileInputStream.read(inputByteArray); } finally { IOUtils.closeQuietly(fileInputStream); } // find the beginning of the http body String inputString = new String(inputByteArray, 0, n, "UTF-8"); int bodyStartPos = inputString.indexOf("\r\n\r\n") + 4; if (bodyStartPos <= 4) { System.out.println("Malformed input file: no blank line between header and content" + inputString); return null; } // get the actual content length and then place it in the header int contentLength = inputString.length() - bodyStartPos; inputString = inputString.replaceFirst("Content-Length: [0-9]+", "Content-Length: " + contentLength); return inputString; } }