/*
 *  PHEX - The pure-java Gnutella-servent.
 *  Copyright (C) 2001 - 2004 Gregor Koukkoullis ( phex <at> kouk <dot> de )
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
package phex.connection;


import java.io.*;
import java.net.*;


import phex.common.*;
import phex.host.*;
import phex.utils.*;


public final class SocketProvider
{
    private SocketProvider()
    {// dont allow instances
    }

    public static Socket connect( HostAddress address ) throws IOException
    {
        return connect( address.getHostName(), address.getPort(), ServiceManager.sCfg.mSocketTimeout );
    }

    public static Socket connect( HostAddress address, int timeout )
        throws IOException
    {
        return connect( address.getHostName(), address.getPort(), timeout );
    }

    /**
     * Opens a socket with a timeout in millis
     */
    public static Socket connect( String host, int port, int timeout )
        throws IOException
    {
        if (port < 0 || port > 0xFFFF)
        {
            throw new IOException("Wrong host address (port out of range: "
                + port + " )" );
        }
        if (ServiceManager.sCfg.mProxyUse)
        {
            return connectSock5(host, port);
        }
        Socket socket = new Socket();
        socket.connect( new InetSocketAddress( host, port ), timeout );
        setSocketTimeout( socket, socket.getInetAddress().getAddress() );
        return socket;
    }

    private static Socket connectSock5(String host, int port)
            throws IOException
    {
        Socket sock = null;
        InputStream is = null;
        OutputStream os = null;

        try
        {
            Socket socket = new Socket();
            socket.connect( new InetSocketAddress( ServiceManager.sCfg.mProxyHost,
                    ServiceManager.sCfg.mProxyPort ), ServiceManager.sCfg.mSocketTimeout );
            setSocketTimeout( sock, sock.getInetAddress().getAddress() );
            is = sock.getInputStream();
            os = sock.getOutputStream();

            byte[] header;
            if (ServiceManager.sCfg.mProxyUserName.length() > 0)
            {
                header = new byte[4];
                header[0] = (byte)0x05; // version
                header[1] = (byte)0x02; // method counts
                header[2] = (byte)0x00; // method no authentication
                header[3] = (byte)0x02; // method user/pw authentication
            }
            else
            {
                header = new byte[3];
                header[0] = (byte)0x05; // version
                header[1] = (byte)0x01; // method counts
                header[2] = (byte)0x00; // method no authentication
            }
            os.write( header, 0, header.length );
            
            int servVersion = is.read();
            if ( servVersion != 0x05 )
            {
                throw new IOException("Invalid SOCKS server version: " + servVersion);
                /*StringBuffer buffer = new StringBuffer( );
                buffer.append( (char) servVersion );
                while ( servVersion != -1 )
                {
                    servVersion = is.read();
                    buffer.append( (char) servVersion );
                }
                throw new IOException("Invalid response from Socks5 proxy server: " +
                    buffer.toString() );
                */
            }

            byte servMethod = (byte)is.read();
            if ( servMethod == (byte)0xFF )
            {
                throw new IOException("SOCKS: No acceptable authentication.");
            }
            if ( servMethod == 0x00 )
            {// no authentication..
            }
            else if ( servMethod == 0x02 )
            {
                authenticateUserPassword( is, os );
            }
            else
            {
                throw new IOException("Unknown SOCKS5 authentication method required.");
            }

            // send request...
            byte[] request = new byte[ 10 ];
            request[ 0 ] = (byte)0x05; // version
            request[ 1 ] = (byte)0x01; // command connect
            request[ 2 ] = (byte)0x00; // reserved
            request[ 3 ] = (byte)0x01; // address type IPv4
            IOUtil.serializeIP( host, request, 4 );
            request[ 8 ] = (byte)(port >> 8); // port
            request[ 9 ] = (byte)(port); // port
            os.write( request, 0, request.length );

            // reply...
            int version = is.read(); // version
            int status = is.read(); // status
            is.read(); // reserved
            int atype = is.read(); // address type

            if (atype == 1)
            {// ipv4 address
                is.read();
                is.read();
                is.read();
                is.read();
            }
            else if (atype == 3)
            {// domain name
                int len = is.read();
                if (len < 0)
                    len += 256;
                while (len > 0)
                {
                    is.read();
                    len--;
                }
            }
            else if (atype == 4)
            {// ipv6 address
                for (int i = 0; i < 16; i++)
                    is.read();
            }
            else
            {
                throw new IOException("Invalid return address type for SOCKS5");
            }
            is.read(); // port
            is.read(); // port
            
            if ( version != 0x05 )
            {
                throw new IOException("Invalid SOCKS server version: " + version);
            }

            switch ( status  )
            {
                case 0x00:
                    return sock;
                case 0x01:
                    throw new IOException( "SOCKS: General SOCKS server failure" );
                case 0x02:
                    throw new IOException( "SOCKS: Connection not allowed by ruleset" );
                case 0x03:
                    throw new IOException( "SOCKS: Network unreachable" );
                case 0x04:
                    throw new IOException( "SOCKS: Host unreachable" );
                case 0x05:
                    throw new IOException( "SOCKS: Connection refused" );
                case 0x06:
                    throw new IOException( "SOCKS: TTL expired" );
                case 0x07:
                    throw new IOException( "SOCKS: Command not supported" );
                case 0x08:
                    throw new IOException( "SOCKS: Address type not supported" );
            }
            throw new IOException("SOCKS: Unknown status response: " + status);
        }
        catch ( Exception exp )
        {
            if (is != null)
            {
                try
                {
                    is.close();
                }
                catch (Exception e2)
                {
                }
            }
            if (os != null)
            {
                try
                {
                    os.close();
                }
                catch (Exception e2)
                {
                }
            }
            if (sock != null)
            {
                try
                {
                    sock.close();
                }
                catch (Exception e2)
                {
                }
            }
            if ( exp instanceof IOException )
            {
                throw (IOException)exp;
            }
            else
            {
                throw new IOException("Error: " + exp.getMessage());
            }
        }
    }


    private static void authenticateUserPassword(InputStream is, OutputStream os )
            throws IOException
    {
        String userName = ServiceManager.sCfg.mProxyUserName;
        String password = ServiceManager.sCfg.mProxyPassword;
        byte[] buffer = new byte[ 3 + userName.length() +  password.length() ];
        
        int pos = 0;
        buffer[ pos++ ] = (byte)0x01;
        buffer[ pos++ ] = (byte)userName.length();
        pos = IOUtil.serializeString( userName, buffer, pos );
        buffer[ pos++ ] = (byte)password.length();
        pos = IOUtil.serializeString( password, buffer, pos );
        os.write( buffer, 0, pos );

        if (is.read() == 1 && is.read() == 0)
        {
            return;
        }

        throw new IOException("Proxy server authentication failed.");
    }

    /**
     * Sets the socket timeout depending on the ip of the host
     * private and invalid IP's get a short time out because they should
     * be reachable very fast in a LAN or are not valid.
     * Unfortunatly this is only for reading not for connecting!
     */
    // TODO check again for jdk 1.4 there are more options to the socket connection
    public static void setSocketTimeout( Socket socket, byte[] ip )
        throws IOException
    {
        // TODO imporve this would interrupt readings too soon.
        // the whole read worker should be reworked to make this efficient
        /*if ( IPUtils.isPrivateIP( ip ) || IPUtils.isInvalidIP( ip ) )
        {
            //System.out.println( "short " + new String( (byte)ip[0] + "." + (int)ip[1] +
            //    "." + (int)ip[2] + "." + (int)ip[3] ));
            // set short timeout
            socket.setSoTimeout( ServiceManager.sCfg.privateSocketTimeout );
        }
        else*/
        {
            //System.out.println( "long " + new String( (int)ip[0] + "." + (int)ip[1] +
            //    "." + (int)ip[2] + "." + (int)ip[3] ));
            // set standard timeout
            socket.setSoTimeout( ServiceManager.sCfg.mSocketTimeout );
        }
    }
}