/*
A very simple self-standing HTTP snoop proxy. Only supports GET and POST requests. Supports verbatim header copying in both directions, with minimal modifications (remove/change HTTP/1.1- and proxy-specific headers)

Usage: java HTTPSnoopProxy [<port>] [<file output allowed>]

Note that [<file output allowed>] can be anything, it's just it's presence that counts..

Author: Werner Ruotsalainen
Version: 1.2
Last modified: 04/30/2003

Modifications:
1.1: added verbatim header copying in both directions
1.2: abandoned java.net.HttpURLConnection completely - got tired of the unexplicable socket write errors. No such errors with plain java.net.Socket's. Rewritten the entire proxy to use plain Sockets.

*/

import java.io.*;
import java.util.*;
import java.net.*;

class HTTPSnoopProxy extends Thread
{
 BufferedInputStream clientIS;
 BufferedOutputStream clientOS;
 Socket socketToClient;
 static boolean fileDebug;
 
 HTTPSnoopProxy(Socket soc) throws Exception
 {
	clientIS = new BufferedInputStream (soc.getInputStream());
	clientOS = new BufferedOutputStream (soc.getOutputStream());
	socketToClient = soc;
 }
 
 public void run()
 {
   try{
 	Vector requestHeaders = new Vector ();
 	DataInputStream clientDIS = new DataInputStream (clientIS);
 	String line;
 	PrintStream debugRequestPS = null;
 	String nextLogFileCount = returnNextLogFileCount();
 	if (fileDebug) debugRequestPS = new PrintStream (new FileOutputStream ("Request."+nextLogFileCount));
 	while (!(line = clientDIS.readLine()).equals("")) // NOT a null-check, just check for the end of the headers! With POST requests, we will read the Content-Length header and the body later
{//System.out.println(line);
 		requestHeaders.add(line);
 	}

 	StringTokenizer st = new StringTokenizer((String)requestHeaders.elementAt(0), " ");
 	String action = (String)st.nextElement(); // GET/POST
 	String url = (String)st.nextElement();
 	//System.out.println("req to: "+url);
 	
 	// getting the server and the page from the URL
 	int serverStart = url.substring(0,10).lastIndexOf("/")+1;
 	int serverEnd = url.indexOf("/", serverStart);
 	if (serverEnd==-1) // e.g. http://localhost - no trailing /
 		serverEnd = url.length();
 	String server = url.substring(serverStart, serverEnd);
 	int serverPort = 80; 
 	if (server.indexOf(":") != -1)
 	{
 		serverPort = Integer.parseInt(server.substring(server.indexOf(":")+1));
 		server = server.substring(0, server.indexOf(":"));
 	}
	int requestedPageStart = serverEnd; // we also put the leading / in it
	String requestedPageURI = "/";
	if (requestedPageStart < url.length()) // ==: no page at all - there wasn't even a /
		requestedPageURI = url.substring(requestedPageStart, url.length());

	Socket socketToServer = new Socket (server, serverPort);
	PrintStream serverOS = new PrintStream (socketToServer.getOutputStream());
	DataInputStream serverDIS = new DataInputStream(socketToServer.getInputStream()); // may be textual because we only send ASCII headers and, in POST mode, URL encoded ASCII

	// print out the first req	
	serverOS.println(action+" "+requestedPageURI+" HTTP/1.0");
	if (debugRequestPS != null) debugRequestPS.println(action+" "+requestedPageURI+" HTTP/1.0");	
	// everything should be sent over except for headers containing 'Connection'
	for (int i=1; i<requestHeaders.size(); i++) // 1: we don't copy the first row
	{
		String actualHeader = (String)requestHeaders.elementAt(i);
		if (actualHeader.toLowerCase().indexOf("connection") == -1) 
		{
			serverOS.println(actualHeader);
			if (debugRequestPS != null) debugRequestPS.println(actualHeader);
		}
	}
	serverOS.println("Connection: close");// send over 1.0
	if (debugRequestPS != null) debugRequestPS.println("Connection: close");	
	serverOS.println(); // end of headers
	if (debugRequestPS != null) debugRequestPS.println();	

	// if it's a POST, we also add the body to the request
	if (action.equals("POST")) 
	{
		int postInfoLength = -1;
		for (int i=0; i<requestHeaders.size(); i++) 
		{
			String actualHeader = (String)requestHeaders.elementAt(i);
			if (actualHeader.toLowerCase().startsWith("content-length"))
				postInfoLength = Integer.parseInt(actualHeader.substring(actualHeader.indexOf(":") + 2, actualHeader.length()));
		}
		byte[] postInfo = new byte[postInfoLength];
		clientDIS.read(postInfo);
		//System.out.println("POSTed: "+new String(postInfo)+"\nto: "+url);
		if (debugRequestPS != null) debugRequestPS.println(new String(postInfo));
		serverOS.print(new String(postInfo));
		serverOS.flush();
	}
	if (debugRequestPS != null) debugRequestPS.close();

	PrintStream debugResponsePS = null;
 	if (fileDebug) 
 		debugResponsePS = new PrintStream (new FileOutputStream ("Response."+nextLogFileCount));

	// we'll need the ret code to decide whether we need to read further so we read the 1st header row separately
	String firstRetHdrLine = serverDIS.readLine();
	if (debugResponsePS != null) debugResponsePS.println(firstRetHdrLine);
	clientOS.write((firstRetHdrLine+"\r\n").getBytes());
	
	st = new StringTokenizer(firstRetHdrLine, " ");
	st.nextElement(); // step over HTTP/1.X
	int responseCode = Integer.parseInt((String)st.nextElement());
	// read back the rest of the headers
	while (!(line = serverDIS.readLine()).equals("")) // NOT a null-check, just check for the end of the headers! With POST requests, we will read the Content-Length header and the body later
 	{
 		if (debugResponsePS != null) debugResponsePS.println(line);
 		clientOS.write((line+"\r\n").getBytes());
 	}
 	clientOS.write("\r\n".getBytes()); // end of headers
 	if (debugResponsePS != null) debugResponsePS.println(); 

	// now, return if the responseCode is anything other than 200 OK (in these cases, there is no body - only headers are sent back)
	if (responseCode != 200) 
	{
		clientOS.flush();
 		clientOS.close();
		return; // anything but 200 means instant return; MWC will know what to do with cases like these
	}

	// start reading serverDIS in binary mode until -1
	int bytesRead;
	int BUFFER_SIZE = 1024;	
 	byte[] buffer = new byte[BUFFER_SIZE];
 	while ((bytesRead = serverDIS.read(buffer)) != -1)
 	{
	 	if (debugResponsePS != null) debugResponsePS.write(buffer, 0, bytesRead);
		clientOS.write(buffer, 0, bytesRead);
	}
	if (debugResponsePS != null) {
		debugResponsePS.flush();
		debugResponsePS.close();
	}
	clientOS.flush();
 	clientOS.close();
 	socketToServer.close();
 	socketToClient.close();
   }catch(Exception e) {e.printStackTrace();}
 }

static int logFileCount;
	String returnNextLogFileCount() // a method for pretty-formatted log files
 	{
 		String nextNumberString = ""+logFileCount++;
 		if (nextNumberString.length() == 1) nextNumberString = "00000"+nextNumberString;
 		if (nextNumberString.length() == 2) nextNumberString = "0000"+nextNumberString;
 		if (nextNumberString.length() == 3) nextNumberString = "000"+nextNumberString;
 		if (nextNumberString.length() == 4) nextNumberString = "00"+nextNumberString; 		
 		if (nextNumberString.length() == 5) nextNumberString = "0"+nextNumberString; 		
 		return nextNumberString;
 	}
 
public static void main(java.lang.String[] args) throws Exception
  {
	if (args.length>1) fileDebug=true;
  	ServerSocket ss = new ServerSocket (Integer.parseInt(args[0]));
  	while(true)
		new HTTPSnoopProxy(ss.accept()).start();
 	}
}
