/**
 * Copyright 2003-2016 SSHTOOLS Limited. All Rights Reserved.
 *
 * For product documentation visit https://www.sshtools.com/
 *
 * This file is part of J2SSH Maverick.
 *
 * J2SSH Maverick is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * J2SSH Maverick 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 J2SSH Maverick.  If not, see <http://www.gnu.org/licenses/>.
 */
/**
 * This file is originally from the http://sourceforge.net/projects/jsocks/
 * released under the LGPL.
 */
package socks;
import socks.server.*;
import java.net.*;
import java.io.*;

/**
 UDP Relay server, used by ProxyServer to perform udp forwarding.
*/
class UDPRelayServer implements Runnable{


    DatagramSocket client_sock; 
    DatagramSocket remote_sock;

    Socket controlConnection;

    int relayPort;
    InetAddress relayIP;

    Thread pipe_thread1,pipe_thread2;
    Thread master_thread;

    ServerAuthenticator auth;

    long lastReadTime;

    static PrintStream log = null;
    static Proxy proxy = null;
    static int datagramSize = 0xFFFF;//64K, a bit more than max udp size
    static int iddleTimeout = 180000;//3 minutes


    /**
      Constructs UDP relay server to communicate with client
      on given ip and port.
      @param clientIP Address of the client from whom datagrams
      will be recieved and to whom they will be forwarded.
      @param clientPort Clients port.
      @param master_thread Thread which will be interrupted, when
      UDP relay server stoppes for some reason.
      @param controlConnection Socket which will be closed, before
      interrupting the master thread, it is introduced due to a bug
      in windows JVM which does not throw InterruptedIOException in
      threads which block in I/O operation.
    */
    public UDPRelayServer(InetAddress clientIP,int clientPort,
                          Thread master_thread,
                          Socket controlConnection,
                          ServerAuthenticator auth)
                          throws IOException{
       this.master_thread = master_thread;
       this.controlConnection = controlConnection;
       this.auth = auth;

       client_sock = new Socks5DatagramSocket(true,auth.getUdpEncapsulation(),
                                              clientIP,clientPort);
       relayPort = client_sock.getLocalPort();
       relayIP   = client_sock.getLocalAddress();

       if(relayIP.getHostAddress().equals("0.0.0.0"))
         relayIP   = InetAddress.getLocalHost();

       if(proxy == null)
          remote_sock = new DatagramSocket();
       else
          remote_sock = new Socks5DatagramSocket(proxy,0,null);
    }


//Public methods
/////////////////


   /**
    Sets the timeout for UDPRelay server.<br>
    Zero timeout implies infinity.<br>
    Default timeout is 3 minutes.
    */

    static public void setTimeout(int timeout){
      iddleTimeout = timeout;
    }


   /**
     Sets the size of the datagrams used in the UDPRelayServer.<br>
     Default size is 64K, a bit more than maximum possible size of the
     datagram.
    */
    static public void setDatagramSize(int size){
      datagramSize = size;
    }

    /**
      Port to which client should send datagram for association.
    */
    public int getRelayPort(){
       return relayPort;
    }
    /**
     IP address to which client should send datagrams for association.
    */
    public InetAddress getRelayIP(){
       return relayIP;
    }

    /**
      Starts udp relay server.
      Spawns two threads of execution and returns.
    */
    public void start() throws IOException{
       remote_sock.setSoTimeout(iddleTimeout);
       client_sock.setSoTimeout(iddleTimeout);

       log("Starting UDP relay server on "+relayIP+":"+relayPort);
       log("Remote socket "+remote_sock.getLocalAddress()+":"+
                            remote_sock.getLocalPort());

       pipe_thread1 = new Thread(this,"pipe1");
       pipe_thread2 = new Thread(this,"pipe2");

       lastReadTime = System.currentTimeMillis();

       pipe_thread1.start();
       pipe_thread2.start();
    }

    /**
     Stops Relay server.
     <p>
     Does not close control connection, does not interrupt master_thread.
    */
    public synchronized void stop(){
       master_thread = null;
       controlConnection = null;
       abort();
    }

//Runnable interface
////////////////////
    public void run(){
       try{
          if(Thread.currentThread().getName().equals("pipe1"))
             pipe(remote_sock,client_sock,false);
          else
             pipe(client_sock,remote_sock,true);
       }catch(IOException ioe){
       }finally{
          abort();
          log("UDP Pipe thread "+Thread.currentThread().getName()+" stopped.");
       }

    }

//Private methods
/////////////////
    private synchronized void abort(){
       if(pipe_thread1 == null) return;

       log("Aborting UDP Relay Server");

       remote_sock.close();
       client_sock.close();

       if(controlConnection != null) 
          try{ controlConnection.close();} catch(IOException ioe){}

       if(master_thread!=null) master_thread.interrupt();

       pipe_thread1.interrupt();
       pipe_thread2.interrupt();

       pipe_thread1 = null;
    }


    static private void log(String s){
      if(log != null){
        log.println(s);
        log.flush();
      }
    }

    private void pipe(DatagramSocket from,DatagramSocket to,boolean out)
                             throws IOException{
       byte[] data = new byte[datagramSize];
       DatagramPacket dp = new DatagramPacket(data,data.length);

       while(true){
          try{
            from.receive(dp);
            lastReadTime = System.currentTimeMillis();

            if(auth.checkRequest(dp,out))
               to.send(dp);

          }catch(UnknownHostException uhe){
            log("Dropping datagram for unknown host");
          }catch(InterruptedIOException iioe){
            //log("Interrupted: "+iioe);
            //If we were interrupted by other thread.
            if(iddleTimeout == 0) return;

            //If last datagram was received, long time ago, return.
            long timeSinceRead = System.currentTimeMillis() - lastReadTime;
            if(timeSinceRead >= iddleTimeout -100) //-100 for adjustment
               return;
          }
          dp.setLength(data.length);
       }
    }
}
