package simplejavaproxy;

/*
 * This is a simple multi-threaded Java proxy server
 * for HTTP requests (HTTPS doesn't seem to work, because
 * the CONNECT requests aren't always handled properly).
 * I implemented the class as a thread so you can call it
 * from other programs and kill it, if necessary (by using
 * the closeSocket() method).
 *
 * We'll call this the 1.1 version of this class. All I
 * changed was to separate the HTTP header elements with
 * \r\n instead of just \n, to comply with the official
 * HTTP specification.
 *
 * This can be used either as a direct proxy to other
 * servers, or as a forwarding proxy to another proxy
 * server. This makes it useful if you want to monitor
 * traffic going to and from a proxy server (for example,
 * you can run this on your local machine and set the
 * fwdServer and fwdPort to a real proxy server, and then
 * tell your browser to use "localhost" as the proxy, and
 * you can watch the browser traffic going in and out).
 *
 * One limitation of this implementation is that it doesn't
 * close the ProxyThread socket if the client disconnects
 * or the server never responds, so you could end up with
 * a bunch of loose threads running amuck and waiting for
 * connections. As a band-aid, you can set the server socket
 * to timeout after a certain amount of time (use the
 * setTimeout() method in the ProxyThread class), although
 * this can cause false timeouts if a remote server is simply
 * slow to respond.
 *
 * Another thing is that it doesn't limit the number of
 * socket threads it will create, so if you use this on a
 * really busy machine that processed a bunch of requests,
 * you may have problems. You should use thread pools if
 * you're going to try something like this in a "real"
 * application.
 *
 * Note that if you're using the "main" method to run this
 * by itself and you don't need the debug output, it will
 * run a bit faster if you pipe the std output to 'nul'.
 *
 * You may use this code as you wish, just don't pretend
 * that you wrote it yourself, and don't hold me liable for
 * anything that it does or doesn't do. If you're feeling
 * especially honest, please include a link to nsftools.com
 * along with the code. Thanks, and good luck.
 *
 * Julian Robichaux -- http://www.nsftools.com
 */
import java.io.*;
import java.net.*;
import java.lang.reflect.Array;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class SimpleProxy extends Thread {
    public static final int DEFAULT_PORT = 8080;
    private ServerSocket server = null;
    private int thisPort = DEFAULT_PORT;
    private String fwdServer = "";
    private int fwdPort = 0;
    private int ptTimeout = ProxyThread.DEFAULT_TIMEOUT;
    private int debugLevel = 1;
    private PrintStream debugOut = System.out;
    //FMG>>
    public static String safariKey = "";
    public static Map cookieMap = new ConcurrentHashMap();


    /* here's a main method, in case you want to run this by itself */
    public static void main(String args[]) {
        int port = 0;
        String fwdProxyServer = "";
        int fwdProxyPort = 0;

        if (args.length == 0) {
            System.err.println("USAGE: java jProxy <port number> [<fwd proxy> <fwd port>]");
            System.err.println("  <port number>   the port this service listens on");
            System.err.println("  <fwd proxy>     optional proxy server to forward requests to");
            System.err.println("  <fwd port>      the port that the optional proxy server is on");
            System.err.println("\nHINT: if you don't want to see all the debug information flying by,");
            System.err.println("you can pipe the output to a file or to 'nul' using \">\". For example:");
            System.err.println("  to send output to the file prox.txt: java jProxy 8080 > prox.txt");
            System.err.println("  to make the output go away: java jProxy 8080 > nul");
            return;
        }

        // get the command-line parameters
        port = Integer.parseInt(args[0]);
        if (args.length > 2) {
            fwdProxyServer = args[1];
            fwdProxyPort = Integer.parseInt(args[2]);
        }

        // create and start the jProxy thread, using a 20 second timeout
        // value to keep the threads from piling up too much
        System.err.println("  **  Starting jProxy on port " + port + ". Press CTRL-C to end.  **\n");
        SimpleProxy jp = new SimpleProxy(port, fwdProxyServer, fwdProxyPort, 20);
        jp.setDebug(1, System.out);		// or set the debug level to 2 for tons of output
        jp.start();

        // run forever; if you were calling this class from another
        // program and you wanted to stop the jProxy thread at some
        // point, you could write a loop that waits for a certain
        // condition and then calls jProxy.closeSocket() to kill
        // the running jProxy thread
        while (true) {
            try {
                Thread.sleep(3000);
            } catch (Exception e) {
            }
        }

        // if we ever had a condition that stopped the loop above,
        // we'd want to do this to kill the running thread
        //jp.closeSocket();
        //return;
    }


    /* the proxy server just listens for connections and creates
     * a new thread for each connection attempt (the ProxyThread
     * class really does all the work)
     */
    public SimpleProxy(int port) {
        thisPort = port;
    }

    public SimpleProxy(int port, String proxyServer, int proxyPort) {
        thisPort = port;
        fwdServer = proxyServer;
        fwdPort = proxyPort;
    }

    public SimpleProxy(int port, String proxyServer, int proxyPort, int timeout) {
        thisPort = port;
        fwdServer = proxyServer;
        fwdPort = proxyPort;
        ptTimeout = timeout;
    }


    /* allow the user to decide whether or not to send debug
     * output to the console or some other PrintStream
     */
    public void setDebug(int level, PrintStream out) {
        debugLevel = level;
        debugOut = out;
    }


    /* get the port that we're supposed to be listening on
     */
    public int getPort() {
        return thisPort;
    }


    /* return whether or not the socket is currently open
     */
    public boolean isRunning() {
        if (server == null) {
            return false;
        } else {
            return true;
        }
    }


    /* closeSocket will close the open ServerSocket; use this
     * to halt a running jProxy thread
     */
    public void closeSocket() {
        try {
            // close the open server socket
            server.close();
            // send it a message to make it stop waiting immediately
            // (not really necessary)
			/*Socket s = new Socket("localhost", thisPort);
            OutputStream os = s.getOutputStream();
            os.write((byte)0);
            os.close();
            s.close();*/
        } catch (Exception e) {
            if (debugLevel > 0) {
                debugOut.println(e);
            }
        }

        server = null;
    }

    public void run() {
        try {
            // create a server socket, and loop forever listening for
            // client connections
            server = new ServerSocket(thisPort);
            if (debugLevel > 0) {
                debugOut.println("Started jProxy on port " + thisPort);
            }

            while (true) {
                Socket client = server.accept();
                ProxyThread t = new ProxyThread(client, fwdServer, fwdPort);
                t.setDebug(debugLevel, debugOut);
                t.setTimeout(ptTimeout);
                t.start();
            }
        } catch (Exception e) {
            if (debugLevel > 0) {
                debugOut.println("jProxy Thread error: " + e);
            }
        }

        closeSocket();
    }
}


/*
 * The ProxyThread will take an HTTP request from the client
 * socket and send it to either the server that the client is
 * trying to contact, or another proxy server
 */
class ProxyThread extends Thread {
    private Socket pSocket;
    private String fwdServer = "";
    private int fwdPort = 0;
    private int debugLevel = 0;
    private PrintStream debugOut = System.out;
    // the socketTimeout is used to time out the connection to
    // the remote server after a certain period of inactivity;
    // the value is in milliseconds -- use zero if you don't want
    // a timeout
    public static final int DEFAULT_TIMEOUT = 20 * 1000;
    private int socketTimeout = DEFAULT_TIMEOUT;

    public ProxyThread(Socket s) {
        pSocket = s;
    }

    public ProxyThread(Socket s, String proxy, int port) {
        pSocket = s;
        fwdServer = proxy;
        fwdPort = port;
    }

    public void setTimeout(int timeout) {
        // assume that the user will pass the timeout value
        // in seconds (because that's just more intuitive)
        socketTimeout = timeout * 1000;
    }

    public void setDebug(int level, PrintStream out) {
        debugLevel = level;
        debugOut = out;
    }

    public void run() {
        try {
            long startTime = System.currentTimeMillis();

            // client streams (make sure you're using streams that use
            // byte arrays, so things like GIF and JPEG files and file
            // downloads will transfer properly)
            BufferedInputStream clientIn = new BufferedInputStream(pSocket.getInputStream());
            BufferedOutputStream clientOut = new BufferedOutputStream(pSocket.getOutputStream());

            // the socket to the remote server
            Socket server = null;

            // other variables
            byte[] request = null;
            byte[] response = null;
            int requestLength = 0;
            int responseLength = 0;
            int pos = -1;
            StringBuffer host = new StringBuffer("");
            String hostName = "";
            int hostPort = 80;

            // get the header info (the web browser won't disconnect after
            // it's sent a request, so make sure the waitForDisconnect
            // parameter is false)
            request = getHTTPData(clientIn, host, false);
            requestLength = Array.getLength(request);

            // separate the host name from the host port, if necessary
            // (like if it's "servername:8000")
            hostName = host.toString();
            pos = hostName.indexOf(":");
            if (pos > 0) {
                try {
                    hostPort = Integer.parseInt(hostName.substring(pos + 1));
                } catch (Exception e) {
                }
                hostName = hostName.substring(0, pos);
            }

            // either forward this request to another proxy server or
            // send it straight to the Host
            try {
                if ((fwdServer.length() > 0) && (fwdPort > 0)) {
                    server = new Socket(fwdServer, fwdPort);
                } else {
                    server = new Socket(hostName, hostPort);
                }
            } catch (Exception e) {
                // tell the client there was an error
                String errMsg = "HTTP/1.0 500\nContent Type: text/plain\n\n"
                        + "Error connecting to the server:\n" + e + "\n";
                clientOut.write(errMsg.getBytes(), 0, errMsg.length());
            }

            if (server != null) {
                server.setSoTimeout(socketTimeout);
                BufferedInputStream serverIn = new BufferedInputStream(server.getInputStream());
                BufferedOutputStream serverOut = new BufferedOutputStream(server.getOutputStream());

                // send the request out
                serverOut.write(request, 0, requestLength);
                serverOut.flush();

                // and get the response; if we're not at a debug level that
                // requires us to return the data in the response, just stream
                // it back to the client to save ourselves from having to
                // create and destroy an unnecessary byte array. Also, we
                // should set the waitForDisconnect parameter to 'true',
                // because some servers (like Google) don't always set the
                // Content-Length header field, so we have to listen until
                // they decide to disconnect (or the connection times out).
                if (debugLevel > 1) {
                    response = getHTTPData(serverIn, true);
                    responseLength = Array.getLength(response);
                } else {
                    responseLength = streamHTTPData(serverIn, clientOut, true);
                }

                serverIn.close();
                serverOut.close();
            }

            // send the response back to the client, if we haven't already
            if (debugLevel > 1) {
                clientOut.write(response, 0, responseLength);
            }

            // if the user wants debug info, send them debug info; however,
            // keep in mind that because we're using threads, the output won't
            // necessarily be synchronous
            if (debugLevel > 5) {
                long endTime = System.currentTimeMillis();
                debugOut.println("Request from " + pSocket.getInetAddress().getHostAddress()
                        + " on Port " + pSocket.getLocalPort()
                        + " to host " + hostName + ":" + hostPort
                        + "\n  (" + requestLength + " bytes sent, "
                        + responseLength + " bytes returned, "
                        + Long.toString(endTime - startTime) + " ms elapsed)");
                debugOut.flush();
            }
            if (debugLevel > 10) {
                debugOut.println("REQUEST:\n" + (new String(request)));
                debugOut.println("RESPONSE:\n" + (new String(response)));
                debugOut.flush();
            }

            // close all the client streams so we can listen again
            clientOut.close();
            clientIn.close();
            pSocket.close();
        } catch (Exception e) {
            if (debugLevel > 0) {
                debugOut.println("Error in ProxyThread: " + e);
            }
            //e.printStackTrace();
        }

    }

    private byte[] getHTTPData(InputStream in, boolean waitForDisconnect) {
        // get the HTTP data from an InputStream, and return it as
        // a byte array
        // the waitForDisconnect parameter tells us what to do in case
        // the HTTP header doesn't specify the Content-Length of the
        // transmission
        StringBuffer foo = new StringBuffer("");
        return getHTTPData(in, foo, waitForDisconnect);
    }

    private byte[] getHTTPData(InputStream in, StringBuffer host, boolean waitForDisconnect) {
        // get the HTTP data from an InputStream, and return it as
        // a byte array, and also return the Host entry in the header,
        // if it's specified -- note that we have to use a StringBuffer
        // for the 'host' variable, because a String won't return any
        // information when it's used as a parameter like that
        ByteArrayOutputStream bs = new ByteArrayOutputStream();
        streamHTTPData(in, bs, host, waitForDisconnect);
        return bs.toByteArray();
    }

    private int streamHTTPData(InputStream in, OutputStream out, boolean waitForDisconnect) {
        StringBuffer foo = new StringBuffer("");
        return streamHTTPData(in, out, foo, waitForDisconnect);
    }

    private int streamHTTPData(InputStream in, OutputStream out, StringBuffer host, boolean waitForDisconnect) {
        // get the HTTP data from an InputStream, and send it to
        // the designated OutputStream
        StringBuffer header = new StringBuffer("");
        String data = "";
        int responseCode = 200;
        int contentLength = 0;
        int pos = -1;
        int byteCount = 0;
        String originalCookies = null;

        try {
            // get the first line of the header, so we know the response code
            data = readLine(in);
            if (data != null) {
                header.append(data + "\r\n");
                pos = data.indexOf(" ");
                if ((data.toLowerCase().startsWith("http")) && (pos >= 0) && (data.indexOf(" ", pos + 1) >= 0)) {
                    String rcString = data.substring(pos + 1, data.indexOf(" ", pos + 1));
                    try {
                        responseCode = Integer.parseInt(rcString);
                    } catch (Exception e) {
                        if (debugLevel > 0) {
                            debugOut.println("Error parsing response code " + rcString);
                        }
                    }
                }
            }

            // get the rest of the header info
            while ((data = readLine(in)) != null) {
                // the header ends at the first blank line
                if (data.length() == 0) {
                    break;
                }
                //header.append(data + "\r\n"); //FMG commentlendi, cookie değiştireceğimiz için aşağıya taşıyorum

                // check for the Host header
                pos = data.toLowerCase().indexOf("host:");
                if (pos >= 0) {
                    host.setLength(0);
                    host.append(data.substring(pos + 5).trim());
                }

                // check for the Content-Length header
                pos = data.toLowerCase().indexOf("content-length:");
                if (pos >= 0) {
                    contentLength = Integer.parseInt(data.substring(pos + 15).trim());
                }

                //**************************************************************
                //FMG>>: simulate browser (User Agent) cookie behaviour
                //We want the server beleive that we are a single browser.

                //Set-Cookie: Safari=cookieversion=2&portal=my&key=E894F897E4A4912E6D683D6939BE74D192731C8F4191A3ACAB6B3FEE16588D2954DBF3B9BF495294EE68A4477E3D772EAA5B0E89433D9926603960942389D153574A4D8BAB4095687832999AECDC2C751104CB8D1A30141A9BCD769E3A126390FFDD02DA9C28307232CD000C64D4F0C3D2993A357CCBED2A19E0467900E3884D71&sessionid=1a923806-a923-4442-9579-325c77ae39db&ref=Google&logged=true&oref=http%3a%2f%2fwww.google.com.tr%2furl%3fsa%3dt%26source%3dweb%26ct%3dres%26cd%3d1%26ved%3d0CAYQFjAA%26url%3dhttp%253A%252F%252Fmy.safaribooksonline.com%252F%26rct%3dj%26q%3dsafari%2bbook%26ei%3d2ZHNS_XcBIX0ObjplcwP%26usg%3dAFQjCNFeqvBpwDvMtcrCBnfHPPiUQofzPA; Path=/; Domain=my.safaribooksonline.com
                //Cookie: __utmv=77706120.B2C-BS-10-M-X; __utma=77706120.1457821667.1270626763.1271763865.1271767431.4; __utmb=77706120; __utmz=77706120.1271763865.3.2.utmccn=(organic)|utmcsr=google|utmctr=safari+book|utmcmd=organic; Safari=cookieversion=2&portal=my&key=E894F897E4A4912E6D683D6939BE74D192731C8F4191A3ACAB6B3FEE16588D2954DBF3B9BF495294EE68A4477E3D772EAA5B0E89433D9926603960942389D153574A4D8BAB4095687832999AECDC2C751104CB8D1A30141A9BCD769E3A126390FFDD02DA9C28307232CD000C64D4F0C3D2993A357CCBED2A19E0467900E3884D71&sessionid=1a923806-a923-4442-9579-325c77ae39db&ref=Google&logged=true&oref=http%3a%2f%2fwww.google.com.tr%2furl%3fsa%3dt%26source%3dweb%26ct%3dres%26cd%3d1%26ved%3d0CAYQFjAA%26url%3dhttp%253A%252F%252Fmy.safaribooksonline.com%252F%26rct%3dj%26q%3dsafari%2bbook%26ei%3d2ZHNS_XcBIX0ObjplcwP%26usg%3dAFQjCNFeqvBpwDvMtcrCBnfHPPiUQofzPA; __utmc=77706120; State=aph=LGOjKGanGVMDHURjGSHANHTCDRCFICGGFLNRPYPXOXVNCCJKAAWOeKHTXMIS&uicode=&PromoCode=my_promo_10&itemsperpage=10&savekey=CB68946AF5AC267A97B7C73B7F8A341F83A137D7B0C6094366303123865F960DD622F95860088400226D1FB16FFCF62ED34A33EFF50B6CC3&HttpReferrer=http%3a%2f%2fmy.safaribooksonline.com%2f0201835959&reader=&action=20&search=safari+book&view=book&displaygrbooks=1&xmlid=0201835959%2f5&isbn=0201835959&omniturexmlidown=1&imagepage=5&portal=my; s_cc=true; s_sq=%5B%5BB%5D%5D

                // check for the Set-Cookie header
                pos = data.toLowerCase().indexOf("set-cookie:");
                if (pos >= 0) {
                    String setCookie = data.substring(pos + 11).trim();
                    handleSetCookie(setCookie);
                }

                //YAP: cookie headerını iptal et, aşağıda biz gönderiyoruz çünkü
                pos = data.toLowerCase().indexOf("cookie:");
                if (pos >= 0) {
                    //we do not send original cookies
                    originalCookies = data.substring(pos + 7);
                } else {
                    header.append(data + "\r\n");
                }

                //<<FMG
                //**************************************************************
            }

            //FMG>>
            //lastly send cookie if applicable for this host
            String cookie = getCookiesForHost(host.toString());
            if (cookie != null) header.append(cookie + "\r\n");
            if (originalCookies != null) System.out.println("\t _ookie:" + originalCookies);
            if (cookie == null && originalCookies != null) header.append("Cookie: " + originalCookies + "\r\n");
            //<<FMG

            // add a blank line to terminate the header info
            header.append("\r\n");

            // convert the header to a byte array, and write it to our stream
            out.write(header.toString().getBytes(), 0, header.length());

            // if the header indicated that this was not a 200 response,
            // just return what we've got if there is no Content-Length,
            // because we may not be getting anything else
            if ((responseCode != 200) && (contentLength == 0)) {
                out.flush();
                return header.length();
            }

            // get the body, if any; we try to use the Content-Length header to
            // determine how much data we're supposed to be getting, because
            // sometimes the client/server won't disconnect after sending us
            // information...
            if (contentLength > 0) {
                waitForDisconnect = false;
            }

            if ((contentLength > 0) || (waitForDisconnect)) {
                try {
                    byte[] buf = new byte[4096];
                    int bytesIn = 0;
                    while (((byteCount < contentLength) || (waitForDisconnect))
                            && ((bytesIn = in.read(buf)) >= 0)) {
                        out.write(buf, 0, bytesIn);
                        byteCount += bytesIn;
                    }
                } catch (Exception e) {
                    String errMsg = "Error getting HTTP body: " + e;
                    if (debugLevel > 0) {
                        debugOut.println(errMsg);
                    }
                    //bs.write(errMsg.getBytes(), 0, errMsg.length());
                }
            }
        } catch (Exception e) {
            if (debugLevel > 0) {
                debugOut.println("Error getting HTTP data: " + e);
            }
            e.printStackTrace();
        }

        //flush the OutputStream and return
        try {
            out.flush();
        } catch (Exception e) {
        }
        return (header.length() + byteCount);
    }

    private String readLine(InputStream in) {
        // reads a line of text from an InputStream
        StringBuffer data = new StringBuffer("");
        int c;

        try {
            // if we have nothing to read, just return null
            in.mark(1);
            if (in.read() == -1) {
                return null;
            } else {
                in.reset();
            }

            while ((c = in.read()) >= 0) {
                // check for an end-of-line character
                if ((c == 0) || (c == 10) || (c == 13)) {
                    break;
                } else {
                    data.append((char)c);
                }
            }

            // deal with the case where the end-of-line terminator is \r\n
            if (c == 13) {
                in.mark(1);
                if (in.read() != 10) {
                    in.reset();
                }
            }
        } catch (Exception e) {
            if (debugLevel > 0) {
                debugOut.println("Error getting header: " + e);
            }
            e.printStackTrace();
        }

        // and return what we have
        return data.toString();
    }

    //FMG>>
    //We are simulating the User Agent role as in RFC2965
    //the cookies coming from server, for a host
    private void handleSetCookie(String setCookie) {
        //global cookie map for each domain
        Map cookieMap = SimpleProxy.cookieMap;

        //parsed attribute value pair for setCookie (we should not send Domain, Path, Expires cookies back to server, so remove them)
        Map cookieValues = parseCookies(setCookie);
        String domain = (String)cookieValues.remove("Domain");
        if (domain == null) domain = (String)cookieValues.remove("domain");
        if (domain == null) domain = "";
        cookieValues.remove("Path");
        cookieValues.remove("Expires");
        cookieValues.remove("path");
        cookieValues.remove("expires");

        //get cookies for this domain
        Map cookiesForDomain = (Map)cookieMap.get(domain);
        //create if not exists
        if (cookiesForDomain == null) cookiesForDomain = new ConcurrentHashMap();
        //put & override the existing values
        cookiesForDomain.putAll(cookieValues);
        //set cookies for this host
        cookieMap.put(domain, cookiesForDomain);

        if (debugLevel > 0) {
            System.out.println(">>>> Got new cookie for domain: '" + domain);
            Iterator it = cookiesForDomain.keySet().iterator();
            while (it.hasNext()) {
                String key = (String)it.next();
                System.out.print("\tKey: " + key);
                System.out.println("  Value: " + cookiesForDomain.get(key));
            }
        }

    }

    private Map parseCookies(String cookies) {
        Map result = new HashMap();
        String[] cookieArray = cookies.split(";");
        for (int i = 0; i < cookieArray.length; i++) {
            String attributeValue = cookieArray[i];
            int eqIndex = attributeValue.indexOf("=");
            if (eqIndex == -1) continue;
            String attribute = attributeValue.substring(0, eqIndex);
            String value = attributeValue.substring(eqIndex + 1);
            result.put(attribute.trim(), value.trim());
        }

        return result;
    }

    //returns the cookies to be sent for a host
    private String getCookiesForHost(String host) {
        //global cookie map for each domain
        Map cookieMap = SimpleProxy.cookieMap;

        //get cookies for this domain
        Map cookiesForDomain = (Map)cookieMap.get(host);
        if (cookiesForDomain == null) return null;

        StringBuffer cookie = new StringBuffer();
        cookie.append("Cookie: ");

        Iterator it = cookiesForDomain.keySet().iterator();
        while (it.hasNext()) {
            String key = (String)it.next();
            cookie.append(key);
            cookie.append("=");
            cookie.append(cookiesForDomain.get(key));
            if (it.hasNext()) cookie.append(";");
        }

        if (debugLevel > 0) {
            System.out.println("Sending cookie for host: " + host);
            System.out.println("\t " + cookie.toString());
        }


        return cookie.toString();
    }
}
