package test;
import java.io.*;
import java.net.*;

/**
  Server to used perform tests for SOCKS library.
*/
public class TestService implements Runnable{
  static final String chargenSequence = " !\"#$%&'()*+,-./0123456789:;<=>?@"+
  "ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefg";

  static final String serviceNames[] = {"echo","discard","chargen","connect"};
  static final int    servicePorts[] = {5678,5679,5680,5681};

  static final int ECHO		= 0;
  static final int DISCARD	= 1;
  static final int CHARGEN	= 2;
  static final int CONNECT	= 3;

  static final int BUF_SIZE	= 1024;

  static final int CHARGEN_WAIT	= 1000;   //1 second
  static final int MAX_WAIT	= 60000;  //1 minute

  static PrintStream log	= null;

  //Class constants
  Socket s;
  int service;

  /**
   Creates new TestService object, which will perform particular
   service on given socket.

   @param s Socket on which to perform service.
   @param service Service which to provide.
  */
  public TestService(Socket s, int service){
    this.s = s;
    this.service = service;
  }

  /**
   Default constructor.
  */
  public TestService(){
    this.s = null;
    this.service = -1;
  }

  public void run(){
     try{
       serve(s,service);
     }catch(IOException ioe){
	 log("Exception:"+ioe);
	 ioe.printStackTrace();
     }
     try{ s.close();}catch(IOException ioe){}
  }

  //Static functions
  /////////////////

  /**
   Maps service name to the integer id, does it in simple
   linear search manner.
   @param serviceName Name of the service whose id one needs.
   @return Integer identifier for this servuce, or -1, if service
           can't be found.
  */
  static public int getServiceId(String serviceName){
     serviceName = serviceName.toLowerCase();
     for(int i = 0;i < serviceNames.length;++i)
        if(serviceName.equals(serviceNames[i]))
           return i;

     //Couldn't find one.
     return -1;
  }

  /**
    Performs given service on given socket.
    <p>
    Simply looks up and calls associated method.
    @param s Socket on which to perform service.
    @param service Id of the service to perform.
    @return true if service have been found, false otherwise.
  */
  static public boolean serve(Socket s, int service) throws IOException{
    switch(service){
      case ECHO:
        echo(s);
      break;
      case DISCARD:
        discard(s);
      break;
      case CHARGEN:
        chargen(s,CHARGEN_WAIT,MAX_WAIT);
      break;
      case CONNECT:
        connect(s);
      break;
      default:
         log("Unknown service:"+service);
	 return false;
    }
     return true;
  }
  /**
    Echos any input on the socket to the output.
    Echo is being done line by line.
    @param s Socket on which to perform service.
  */
  static public void echo(Socket s) throws IOException{
    BufferedReader in = new BufferedReader(new InputStreamReader(
					       s.getInputStream()));
    OutputStream out = s.getOutputStream();

    log("Starting \"echo\" on "+s);

    String line = in.readLine();
    while(line != null){
       out.write((line+"\n").getBytes());
       log(line);
       line = in.readLine();
    }

    log("Echo done.");
  }

  /**
    Reads input from the socket, and does not write anything back.
    logs input in line by line fashion.
    @param s Socket on which to perform service.
  */
  static public void discard(Socket s) throws IOException{
    BufferedReader in = new BufferedReader(new InputStreamReader(
					       s.getInputStream()));
    log("Starting discard on "+s);

    String line = in.readLine();
    while(line != null){
       log(line);
       line = in.readLine();
    }

    log("Discard finished.");
  }

  /**
    Generates characters and sends them to the socket.
    <p>
    Unlike usual chargen (port 19), each next line of the generated
    output is send after increasingly larger time intervals. It starts
    from wait_time (ms), and each next time wait time is doubled.
    Eg. 1 2 4 8 16 32 64 128 256 512 1024 2048 4096 8192 ... well
    you got the idea.
    <p>
    It starts if either connection is clsoed or the wait time grows
    bigger than max_wait.

    @param s Socket on which to perform service.
    @param wait_time Time in ms, from which timing sequence should begin.
    @param max_wait Time in ms, after reaching timeout greater than this
		    value, chargen will stop.
  */
  static public void chargen(Socket s,long wait_time,long max_wait)
		     throws IOException{
    byte[] buf = chargenSequence.getBytes();
    int pos = 0;
    OutputStream out = s.getOutputStream();
    InputStream in = s.getInputStream();
    s.setSoTimeout(100); //0.1 ms

    log("Starting \"chargen\" on "+s);
    while(true){
       log("Sending message.");
       out.write(buf,pos,buf.length - pos);
       out.write(buf,0,pos);       
       out.write("\n".getBytes());
       pos++;
       try{
	  if(wait_time > max_wait) break;

	  log("Going to sleep for "+wait_time+" ms.");
          Thread.currentThread().sleep(wait_time);
	  wait_time *= 2;
	  if(in.read() < 0) break; //Connection closed
       }catch(InterruptedException ie){
       }catch(InterruptedIOException ioe){
       }
    }
    log("Chargen finished.");
  }

  /**
    Models connect back situation.
    <p>
    Reads a line from the socket connection, line should be in the
    form port service_id. Connects back to the remote host to port
    specified in the line, if successful performs a service speciefied
    by id on that new connection. If accept was successful closes the
    control connection, else outputs error message, and then closes
    the connection.

    @param s Control connection.
  */
  static public void connect(Socket s)throws IOException{
    String line = null;
    Socket sock;
    int port;
    int service_id;

    BufferedReader in = new BufferedReader(new InputStreamReader(
					       s.getInputStream()));
    OutputStream out = s.getOutputStream();

    log("Starting \"connect\" on "+s);
    line = in.readLine();
    if(line == null) return; //They closed connection

    java.util.StringTokenizer st = new java.util.StringTokenizer(line);
    if(st.countTokens() < 2){ //We need at least 'port' and "id"
       out.write("Expect: port serviceId.\n".getBytes());
       log("Invalid arguments.");
       return;
    }
    try{
       port = Integer.parseInt(st.nextToken());
       service_id = Integer.parseInt(st.nextToken());
    }catch(NumberFormatException nfe){
       out.write("Expect: port serviceId.\n".getBytes());
       log("Invalid arguments.");
       return;
    }
    try{
      log("Connecting to "+s.getInetAddress()+":"+port);
      sock = new Socket(s.getInetAddress(),port);
    }catch(IOException ioe){
      out.write(("Connect to "+s.getInetAddress()+
		 ":"+port+" failed").getBytes());
      log("Connect failed.");
      return;
    }
    s.close();
    log("About to serve "+service_id);
    serve(sock,service_id);
  }

  /**
    Pipes data from the input stream to the output.
    @param in  Input stream.
    @param out Output stream.
  */
  static public void pipe(InputStream in, OutputStream out)
                          throws IOException{
    byte[] buf = new byte[BUF_SIZE];
    int bread = 0;
    while(bread >= 0){
       bread = in.read(buf);
       out.write(buf,0,bread);
    }
  }

  /**
    Performs logging.
  */
  static synchronized void log(String s){
     if(log != null) log.println(s);
  }

}
