package net.sourceforge.jsocks;

import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.UnknownHostException;

/**
 SOCKS5 request/response message.
*/

public class Socks5Message extends ProxyMessage{
   /** Address type of given message*/
   public int addrType;

   byte[] data;

   /**
    Server error response.
    @param cmd Error code.
   */
   public Socks5Message(int cmd){
      super(cmd,null,0);
      data = new byte[3];
      data[0] = SOCKS_VERSION; //Version.
      data[1] = (byte)cmd;     //Reply code for some kind of failure.
      data[2] = 0;             //Reserved byte.
   }

   /**
    Construct client request or server response.
    @param cmd - Request/Response code.
    @param ip  - IP field.
    @paarm port - port field.
   */
   public Socks5Message(int cmd,InetAddress ip,int port){
      super(cmd,ip,port);
      this.host = ip==null?"0.0.0.0":ip.getHostName();
      this.version = SOCKS_VERSION;

      byte[] addr;

      if(ip == null){
         addr = new byte[4];
         addr[0]=addr[1]=addr[2]=addr[3]=0;
      }else
         addr = ip.getAddress();

      addrType = addr.length == 4 ? SOCKS_ATYP_IPV4
                                  : SOCKS_ATYP_IPV6;
 
      data = new byte[6+addr.length];
      data[0] = (byte) SOCKS_VERSION;		//Version
      data[1] = (byte) command;			//Command
      data[2] = (byte) 0;			//Reserved byte
      data[3] = (byte) addrType;		//Address type
 
      //Put Address
      System.arraycopy(addr,0,data,4,addr.length);
      //Put port
      data[data.length-2] = (byte)(port>>8);
      data[data.length-1] = (byte)(port);
   }


   /**
    Construct client request or server response.
    @param cmd - Request/Response code.
    @param hostName  - IP field as hostName, uses ADDR_TYPE of HOSTNAME.
    @paarm port - port field.
   */
   public Socks5Message(int cmd,String hostName,int port){
      super(cmd,null,port);
      this.host = hostName;
      this.version = SOCKS_VERSION;

      //System.out.println("Doing ATYP_DOMAINNAME");

      addrType = SOCKS_ATYP_DOMAINNAME;
      byte addr[] = hostName.getBytes();
 
      data =new byte[7+addr.length];
      data[0] = (byte) SOCKS_VERSION;		//Version
      data[1] = (byte) command;			//Command
      data[2] = (byte) 0;			//Reserved byte
      data[3] = (byte) SOCKS_ATYP_DOMAINNAME;	//Address type
      data[4] = (byte) addr.length;		//Length of the address

      //Put Address
      System.arraycopy(addr,0,data,5,addr.length);
      //Put port
      data[data.length-2] = (byte)(port >>8);
      data[data.length-1] = (byte)(port);
   }

   /**
     Initialises Message from the stream. Reads server response from
     given stream.
     @param in Input stream to read response from.
     @throws SocksException If server response code is not SOCKS_SUCCESS(0), or
     if any error with protocol occurs.
     @throws IOException If any error happens with I/O.
   */
   public Socks5Message(InputStream in) throws SocksException,
                                              IOException{
      this(in,true);
   }

   /**
     Initialises Message from the stream. Reads server response or client 
     request from given stream.
     
     @param in Input stream to read response from.
     @param clinetMode If true read server response, else read client request.
     @throws SocksException If server response code is not SOCKS_SUCCESS(0) and
     reading in client mode, or if any error with protocol occurs.
     @throws IOException If any error happens with I/O.
   */
   public Socks5Message(InputStream in,boolean clientMode)throws SocksException,
                                              IOException{
      read(in,clientMode);
   }


   /**
     Initialises Message from the stream. Reads server response from
     given stream.
     @param in Input stream to read response from.
     @throws SocksException If server response code is not SOCKS_SUCCESS(0), or
     if any error with protocol occurs.
     @throws IOException If any error happens with I/O.
   */
   public void read(InputStream in) throws SocksException,
                                           IOException{
       read(in,true);
   }


   /**
     Initialises Message from the stream. Reads server response or client 
     request from given stream.
     
     @param in Input stream to read response from.
     @param clinetMode If true read server response, else read client request.
     @throws SocksException If server response code is not SOCKS_SUCCESS(0) and
     reading in client mode, or if any error with protocol occurs.
     @throws IOException If any error happens with I/O.
   */
   public void read(InputStream in,boolean clientMode) throws SocksException,
                                           IOException{
      data = null;
      ip = null;

      DataInputStream di = new DataInputStream(in);

      version = di.readUnsignedByte();
      command = di.readUnsignedByte();
      if(clientMode && command != 0)
        throw new SocksException(command);

      @SuppressWarnings("unused")
      int reserved = di.readUnsignedByte();
      addrType = di.readUnsignedByte();

      byte addr[];

      switch(addrType){
         case SOCKS_ATYP_IPV4:
            addr = new byte[4];
            di.readFully(addr);
            host = bytes2IPV4(addr,0);
         break;
         case SOCKS_ATYP_IPV6:
           addr = new byte[SOCKS_IPV6_LENGTH];//I believe it is 16 bytes,huge!
           di.readFully(addr);
           host = bytes2IPV6(addr,0);
         break;
         case SOCKS_ATYP_DOMAINNAME:
           //System.out.println("Reading ATYP_DOMAINNAME");
           addr = new byte[di.readUnsignedByte()];//Next byte shows the length
           di.readFully(addr);
           host = new String(addr);
         break;
         default:
            throw(new SocksException(Proxy.SOCKS_JUST_ERROR));
      }

      port = di.readUnsignedShort();

      if(addrType != SOCKS_ATYP_DOMAINNAME && doResolveIP){
         try{
            ip = InetAddress.getByName(host);
         }catch(UnknownHostException uh_ex){
         }
      }
   }

   /**
    Writes the message to the stream.
    @param out Output stream to which message should be written.
   */
   public void write(OutputStream out)throws SocksException,
                                             IOException{
     if(data == null){
       Socks5Message msg;

       if(addrType == SOCKS_ATYP_DOMAINNAME)
          msg = new Socks5Message(command,host,port);
       else{
          if(ip == null){
             try{
               ip = InetAddress.getByName(host);
             }catch(UnknownHostException uh_ex){
               throw new SocksException(Proxy.SOCKS_JUST_ERROR);
             }
          }
          msg = new Socks5Message(command,ip,port);
       }
       data = msg.data;
     }
     out.write(data);
   }

   /**
    Returns IP field of the message as IP, if the message was created
    with ATYP of HOSTNAME, it will attempt to resolve the hostname,
    which might fail.
    @throws UnknownHostException if host can't be resolved.
   */
   public InetAddress getInetAddress() throws UnknownHostException{
     if(ip!=null) return ip;

     return (ip=InetAddress.getByName(host));
   }

   /**
     Returns string representation of the message.
   */
   public String toString(){
      String s=
        "Socks5Message:"+"\n"+
        "VN   "+version+"\n"+
        "CMD  "+command+"\n"+
        "ATYP "+addrType+"\n"+
        "ADDR "+host+"\n"+
        "PORT "+port+"\n";
      return s;
   }
            

   /**
    *Wether to resolve hostIP returned from SOCKS server
    *that is wether to create InetAddress object from the
    *hostName string
    */
   static public boolean resolveIP(){ return doResolveIP;}

   /**
    *Wether to resolve hostIP returned from SOCKS server
    *that is wether to create InetAddress object from the
    *hostName string
    *@param doResolve Wether to resolve hostIP from SOCKS server.
    *@return Previous value.
    */
   static public boolean resolveIP(boolean doResolve){
      boolean old = doResolveIP;
      doResolveIP = doResolve;
      return old;
   }

   /*
   private static final void debug(String s){
      if(DEBUG)
         System.out.print(s);
   }
   private static final boolean DEBUG = false;
   */

   //SOCKS5 constants
   public static final int SOCKS_VERSION		=5;

   public static final int SOCKS_ATYP_IPV4		=0x1; //Where is 2??
   public static final int SOCKS_ATYP_DOMAINNAME	=0x3; //!!!!rfc1928
   public static final int SOCKS_ATYP_IPV6		=0x4;

   public static final int SOCKS_IPV6_LENGTH		=16;

   static boolean doResolveIP = true;

}
