// Netzprogrammierung
// Gruppe 01 (Übung 1)
// Aufgabe 1-4

/*
 ProxyServer.java
 
 Anmerkungen:
 - Dem Proxyserver können auf der Kommandozeile optional Port und Debug Level als int-Werte
   übergeben werden (Silent Mode, nur Header, etc.). Als default werden Port 10000 und Debug
   Level 3 genutzt.

 Konzepte:
 - Die Eingabeströme von Client und v.a. Server werden über binäre Inputstreams abgewickelt,
   um Bilder und andere Binärdaten zu erhalten
 - Aus dem Client-Request werden Server und Port geparst, so kann bspw. auch http über Port
   8080 abgefragt werden
 - Der vom Client entgegengenommene Header wird teilweise bereinigt, da er sich an einen
   Proxy richtet (Zeile 148 ff.)
 - Die Antwort vom Server wird über die private Methode readLine() (Z. 242 ff.) abgewickelt, so
   kann der InputStream komfortabel gehandhabt werden
 - Die empfangenen Headerzeilen werden aufsummiert und durch die angeforderten Ressourcen
   geteilt. Das Ergebnis wird über den Header an den Client gesendet (Z. 190 ff.)
 - Die meisten Exceptions werden nur abgefangen, der Proxyserver läuft danach wieder an, da
   die meisten Fehler durch Abbruch der Verbindung durch den Client u.ä. entstehen, bemerkt
   man dies im Betrieb i.d.R. nicht
 - Die einzige gesondert behandelte Exception ist die Connection Exception (Z. 214): Tritt sie
   auf, wird HTTP-Fehlercode 500 an den Client übermittelt
*/

package uebung01.aufgabe04;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.net.ConnectException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.util.Iterator;
import java.util.LinkedList;


/**
 * Ein einfacher Proxy-Server, der die durchschnittliche Anzahl empfangener Headerzeilen ermittelt.
 * 
 * @author Tilman Walther
 */
public class ProxyServer {
	
	// da unser einfacher Proxy die content-length nicht beruecksichtigt (die
	// auch nicht von jedem Server gesendet wird), muessen Verbindungen oft
	// per Timeout beendet werden.
	final int		SERVER_TIMEOUT	= 2500;
	
	// da Java den Linefeed dem System anpasst, nutzen wir einen
	// String, der sich nach dem HTTP-Protokoll richtet
	final String	LF = new String(""+((char) 13)+((char) 10));
	
	long			headerLineCount	= 0;
	long			itemCount		= 0;
	
	StringBuffer 	lineBuffer		= new StringBuffer(600);
	
	public ProxyServer(int port, int message) throws IOException {
		
		System.out.println("PROXY: Starting ProxyServer on port "+port);
		
		ServerSocket				proxySocket		= new ServerSocket(port);
		
		Socket					client;
		BufferedInputStream		clientIn;
		PrintStream				clientOut;
		
		Socket					server;
		BufferedInputStream		serverIn;
		PrintStream				serverOut;
		
		String					request;
		String					domain;
		int						serverport;
		LinkedList				requestStrings	= new LinkedList();
		
		String					s;
		byte[]					buffer			= new byte[1024];
		int						in1;
		long						headerLineAverage;
		
		while (true) {
			
			if (message > 1) System.out.println("PROXY: Listening... (press Ctrl+C to end)");			
			client = proxySocket.accept();
			
			clientIn = new BufferedInputStream(client.getInputStream());
			clientOut = new PrintStream(client.getOutputStream(), true);
			
			try {
				
				// Anfrage vom Browser holen
				s = readLine(clientIn);
				
				// Ressource extrahieren
				request = s.substring(0, s.indexOf(" ")+1) + s.substring(s.indexOf("/", s.indexOf("//")+2));
				
				// Domain und Port extrahieren
				domain = s.substring(s.indexOf("//")+2);
				domain = domain.substring(0, domain.indexOf("/"));
				
				if ((in1 = domain.indexOf((int) ':')) > -1 ) {
					serverport = Integer.parseInt(domain.substring(in1+1));
					domain = domain.substring(0, in1);
				}
				else {
					serverport = 80;
				}
				
				if (message > 1) System.out.print("PROXY: Request from client: "+s);			
				
				// Restlichen Anfrage-Header holen
				requestStrings.clear();
				do {
					s = readLine(clientIn);
					requestStrings.add(s);
					if (message > 4) System.out.print("  "+s);
				} while (!s.equals(LF));
				
				long time = System.currentTimeMillis();
				
				try {
					if (message > 1) System.out.println("PROXY: Connecting server "+domain+":"+serverport);
					
					server = new Socket(domain, serverport);
					server.setSoTimeout(SERVER_TIMEOUT);
					serverIn = new BufferedInputStream(server.getInputStream());
					serverOut = new PrintStream(server.getOutputStream(), true);
					
					if (message > 1) System.out.print("PROXY: Sending request to server: "+request);
					serverOut.print(request);
					
					// Angepassten Header an Server schicken
					Iterator iter = requestStrings.iterator();
					while (iter.hasNext()) {
						
						s = (String) iter.next();
						
						if (s.startsWith("Proxy-Connection")) {
							serverOut.print("Connection: close"+LF);
							if (message > 2) System.out.print("  Connection: close"+LF);
						}
						else if (s.startsWith("Transfer-Encoding")) {
							serverOut.print("Transfer-Encoding: identity"+LF);
							if (message > 2) System.out.print("  Transfer-Encoding: identity"+LF);
						}
						else if (s.startsWith("Accept-Encoding")) {
							
							s = s.substring(0,(s.length()-2)).concat(",identity"+LF);
							//s = "Accept-Encoding: identity"+LF;
							
							serverOut.print(s);
							if (message > 2) System.out.print("  "+s);
						}
						else {
							serverOut.print(s);
							if (message > 2) System.out.print("  "+s);
						}
					}
					
					// Antwort vom Server entgegennehmen
					try {
						
						// Header empfangen
						s = readLine(serverIn);
						
						// --> Hier koennen HTTP response codes abgefangen werden
						if (message > 1) System.out.print("PROXY: Receiving data from server: "+s);
						
						itemCount++;
						
						while (!s.equals(LF)) {
							
							headerLineCount++;
							
							if (message > 2) System.out.print("  "+s);
							clientOut.print(s);
							s = readLine(serverIn);
						}
						
						// durchschnittliche Anzahl der Headerzeilen berechnen
						headerLineAverage = headerLineCount / itemCount;
						
						if (message > 2) System.out.print("  X-mean-headercount: "+ headerLineAverage + LF + LF);
						clientOut.print("X-mean-headercount: "+ headerLineAverage + LF + LF);
						
						// Daten empfangen
						while ((in1 = serverIn.read(buffer)) > 0) {
							if (message > 3) System.out.write(buffer, 0, in1);							
							clientOut.write(buffer, 0, in1);
						}
					}
					catch (SocketTimeoutException ste) {
						if (message > 1) System.err.println("PROXY: Connection timed out");
					}			
					
					if (message > 1) System.out.println("PROXY: Time "+(System.currentTimeMillis()-time)+" ms");
					if (message > 1) System.out.println("PROXY: Unconnect...\n");
					
					serverIn.close();
					serverOut.close();
					server.close();
					
				}
				catch (ConnectException ce) {
					
					if (message > 0) System.err.println("ConnectException, sending code 500 to client");
					buffer = ("HTTP/1.0 500\nContent Type: text/plain\n\nError while connecting server: "+domain+"\n").getBytes();				
					clientOut.write(buffer, 0, ("HTTP/1.0 500\nContent Type: text/plain\n\nError while connecting server: "+domain+"\n").length());
					
				}
				
				clientIn.close();
				clientOut.close();
				client.close();
			}
			catch (SocketException se) {
				if (message > 4) System.err.println("SocketException");
			}
			catch (Exception e) {
				// nothing, just try again
				if (message > 4) e.printStackTrace();
			}
		}
		
		//proxySocket.close();
	}
	
	
	/**
	 * Liest eine Zeile vom binären Eingabestrom.
	 * (Auf diese Weise bleiben binäre Daten korrekt erhalten.)
	 */
	private String readLine(InputStream in) throws IOException
	{
		int i;
		lineBuffer.setLength(0);
		
		while (((i = in.read()) > 0) && (i != 13)){
			lineBuffer.append((char) i);
		}
		
	
		// nach HTTP 1.1 müssten die Zeilenenden mit #13#10 markiert sein
		// (durch die Aufteilung schlucken wir auch Unix/Macintosh)
		if (i == 13) {
			
			lineBuffer.append((char) 13);
			in.mark(1);

			if (in.read() != 10)
				in.reset();
			else
				lineBuffer.append((char) 10);
		}
		
		return lineBuffer.toString();
	}

	
	public static void main(String[] args) throws IOException {

		int port	= 10000;		// default port
		int message = 3;		// default output level
		
		if (args.length > 2) {
			System.out.println("usage: MultServer [port number] [output level]");
		}
		else {
			
			if (args.length > 0) {
				try {
					port = Integer.parseInt(args[0]);
				}
				catch (NumberFormatException nfe) {
					System.out.println(args[0]+" is not a valid port number.\nusage: MultServer [port number] [output level]");
					System.exit(1);
				}
			}
			
			if (args.length > 1) {
				try {
					port = Integer.parseInt(args[1]);
				}
				catch (NumberFormatException nfe) {
					System.out.println(args[0]+" is not a valid port number.\nusage: MultServer [port number] [output level]");
					System.exit(1);
				}
			}
			
		}
		
		new ProxyServer(port, message);
	}
}
