/**
 * An implementation of the Socks4 protocol.  This protocol is
 * not exposed in the library but is included here incase EDS
 * ever needs support for SOCKS4.
 *
 * @see ProxySOCKS5
 */

/*
 This file depends on following documents,
 - SOCKS: A protocol for TCP proxy across firewalls, Ying-Da Lee
 http://www.socks.nec.com/protocol/socks4.protocol
 */

package com.jscape.inet.util;

import com.jscape.util.IOTools;

import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;

public class ProxySOCKS4
        implements Proxy {

    private static final int DEFAULTPORT = 1080;

    private String proxyHost;

    private int proxyPort;

    private String host;

    private int port;

    private InputStream in;

    private OutputStream out;

    private Socket socket;

    private String user;

    private String passwd;

    /**
     * Minimal constructor. assumes default port defined by getDefaultPort();
     *
     * @param proxyHost
     */
    public ProxySOCKS4(String proxyHost) {
        int port = DEFAULTPORT;
        String host = proxyHost;
        if (proxyHost.indexOf(':') != -1) {
            try {
                host = proxyHost.substring(0, proxyHost.indexOf(':'));
                port = Integer.parseInt(proxyHost.substring(proxyHost.indexOf(':') + 1));
            } catch (Exception e) {
                // todo: ignore?
            }
        }
        this.proxyHost = host;
        this.proxyPort = port;
    }

    /**
     * Full constructor overrides default port
     *
     * @param proxyHost
     * @param proxyPort
     */
    public ProxySOCKS4(String proxyHost, int proxyPort) {
        this.proxyHost = proxyHost;
        this.proxyPort = proxyPort;
    }

    /**
     * Sets the user password
     *
     * @param user
     * @param passwd
     */
    public void setUserPasswd(String user, String passwd) {
        this.user = user;
        this.passwd = passwd;
    }

    /**
     * Connects to the proxy with the givem parameters
     *
     * @param socketFactory
     * @param host
     * @param port
     * @param timeout
     */
    public void connect(SocketFactory socketFactory, String host, int port, long timeout)
            throws Exception {
        this.host = host;
        this.port = port;
        try {
            if (socketFactory == null) {
                socket = Util.createSocket(proxyHost, proxyPort, timeout);
                // socket=new Socket(proxy_host, proxy_port);
                in = socket.getInputStream();
                out = socket.getOutputStream();
            } else {
                socket = socketFactory.createSocket(proxyHost, proxyPort);
                in = socketFactory.getInputStream(socket);
                out = socketFactory.getOutputStream(socket);
            }
            if (timeout > 0) {
                socket.setSoTimeout((int) timeout);
            }
            socket.setTcpNoDelay(true);

            byte[] buf = new byte[1024];
            int index = 0;

            /*
             * 1) CONNECT
             *
             * The client connects to the SOCKS server and sends a CONNECT
             * request when it wants to establish a connection to an application
             * server. The client includes in the request packet the IP address
             * and the port number of the destination host, and userid, in the
             * following format.
             *
             * +----+----+----+----+----+----+----+----+----+----+....+----+ |
             * VN | CD | DSTPORT | DSTIP | USERID |NULL|
             * +----+----+----+----+----+----+----+----+----+----+....+----+ #
             * of bytes: 1 1 2 4 variable 1
             *
             * VN is the SOCKS protocol version number and should be 4. CD is
             * the SOCKS command code and should be 1 for CONNECT request. NULL
             * is a byte of all zero bits.
             */

            index = 0;
            buf[index++] = 4;
            buf[index++] = 1;

            buf[index++] = (byte) (port >>> 8);
            buf[index++] = (byte) (port & 0xff);

            try {
                InetAddress addr = InetAddress.getByName(host);
                byte[] byteAddress = addr.getAddress();
                for (int i = 0; i < byteAddress.length; i++) {
                    buf[index++] = byteAddress[i];
                }
            } catch (UnknownHostException uhe) {
                throw new Exception("ProxySOCKS4: " + uhe.toString());
            }

            if (user != null) {
                System.arraycopy(user.getBytes(StandardCharsets.UTF_8), 0, buf, index, user.length());
                index += user.length();
            }
            buf[index++] = 0;
            out.write(buf, 0, index);

            /*
             * The SOCKS server checks to see whether such a request should be
             * granted based on any combination of source IP address,
             * destination IP address, destination port number, the userid, and
             * information it may obtain by consulting IDENT, cf. RFC 1413. If
             * the request is granted, the SOCKS server makes a connection to
             * the specified port of the destination host. A reply packet is
             * sent to the client when this connection is established, or when
             * the request is rejected or the operation fails.
             *
             * +----+----+----+----+----+----+----+----+ | VN | CD | DSTPORT |
             * DSTIP | +----+----+----+----+----+----+----+----+ # of bytes: 1 1
             * 2 4
             *
             * VN is the version of the reply code and should be 0. CD is the
             * result code with one of the following values:
             *
             * 90: request granted 91: request rejected or failed 92: request
             * rejected becasue SOCKS server cannot connect to identd on the
             * client 93: request rejected because the client program and identd
             * report different user-ids
             *
             * The remaining fields are ignored.
             */
            IOTools.read(buf, 0, 8, in);

            if (buf[0] != 0) {
                throw new Exception("ProxySOCKS4: server returns VN " + buf[0]);
            }
            if (buf[1] != 90) {
                try {
                    socket.close();
                } catch (Exception eee) {
                    // todo: ignore?
                }
                throw new Exception("ProxySOCKS4: server returns CD " + buf[1]);
            }
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            try {
                if (socket != null) {
                    socket.close();
                }
            } catch (Exception eee) {
                // todo: ignore?
            }
            throw new Exception("ProxySOCKS4: " + e.toString());
        }
    }

    /**
     * @return InputStream
     */
    public InputStream getInputStream() {
        return in;
    }

    /**
     * @return OutputStream
     */
    public OutputStream getOutputStream() {
        return out;
    }

    /**
     * @return Socket
     */
    public Socket getSocket() {
        return socket;
    }

    /**
     * closes the connection to the proxy
     */
    public void close() {
        try {
            if (in != null) {
                in.close();
            }
            if (out != null) {
                out.close();
            }
            if (socket != null) {
                socket.close();
            }
        } catch (Exception e) {
            // todo: ignore?
        }
        in = null;
        out = null;
        socket = null;
    }

    public static int getDefaultPort() {
        return DEFAULTPORT;
    }
}
