Download | Plain Text | Line Numbers


/*
 * Copyright (c) 2010, Manuel Mausz. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *   - Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *
 *   - Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *
 *   - The names of the authors may not be used to endorse or promote products
 *     derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
 
import static java.lang.System.err;
import static java.lang.System.out;
 
import javax.crypto.*;
import java.security.*;
import org.bouncycastle.openssl.PEMReader;
import org.bouncycastle.util.encoders.Base64;
import org.bouncycastle.openssl.PasswordFinder;
import org.bouncycastle.util.encoders.Hex;
 
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.channels.DatagramChannel;
 
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.MissingResourceException;
import java.util.Collections;
import java.util.Enumeration;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Iterator;
import java.util.HashMap;
import java.util.Arrays;
import java.util.Map;
import java.net.*;
import java.io.*;
 
/*
 * Proxy implementation for Lab#1 of DSLab WS10
 * See angabe.pdf for details
 *
 * This code is not documented at all. This is volitional
 *
 * @author Manuel Mausz (0728348)
 */
public class Proxy
{
  public class FSRecord
    implements Comparable<FSRecord>
  {
    public final String host;
    public final int port;
    public int usage;
    public boolean online;
    public long lastUpdate;
 
    FSRecord(String host, int port)
    {
      this.host  = host;
      this.port  = port;
      usage      = 0;
      online     = true;
      lastUpdate = Calendar.getInstance().getTimeInMillis();
    }
 
    public void ping()
    {
      online = true;
      lastUpdate = Calendar.getInstance().getTimeInMillis();
    }
 
    public boolean equals(FSRecord o)
    {
      return online == o.online && usage == o.usage;
    }
 
    public int compareTo(FSRecord o)
    {
      return usage - o.usage;
    }
  }
 
  public class FSRecords
    extends HashMap<String, FSRecord>
  {}
 
  /*==========================================================================*/
 
  public class UserRecord
  {
    public final String name;
    public String pass;
    public int credits;
    public ArrayList<String> loggedin;
 
    UserRecord(String name, int credits)
    {
      this.name    = name;
      this.pass    = null;
      this.credits = credits;
      loggedin  = new ArrayList<String>();
    }
  }
 
  public class UserRecords
    extends HashMap<String, UserRecord>
  {}
 
  /*==========================================================================*/
 
 
  public class ProxyConnection
    implements Runnable
  {
    private final String host;
    private final int port;
    private FSRecords fileservers;
 
    ProxyConnection(String host, int port, FSRecords fileservers)
    {
      this.host        = host;
      this.port        = port;
      this.fileservers = fileservers;
    }
 
    /*------------------------------------------------------------------------*/
 
    public void run()
    {
      String key = host + ":" + port;
      synchronized(fileservers)
      {
        FSRecord record = fileservers.get(key);
        if (record == null)
        {
          fileservers.put(key, new FSRecord(host, port));
          out.println("New fileserver registered: " + key);
        }
        else
        {
          if (!record.online)
            out.println("Fileserver is online again: " + key);
          record.ping();
        }
      }
    }
  }
 
  /*==========================================================================*/
 
  public class UDPSocketReader
    implements Runnable
  {
    private final DatagramChannel dchannel;
    private FSRecords fileservers;
    private final Object mainLock;
    private final ExecutorService pool;
 
    UDPSocketReader(DatagramChannel dchannel, FSRecords fileservers,
        Object mainLock)
    {
      this.dchannel    = dchannel;
      this.fileservers = fileservers;
      this.mainLock    = mainLock;
      this.pool        = Executors.newCachedThreadPool();
    }
 
    /*------------------------------------------------------------------------*/
 
    public void run()
    {
      try
      {
        String tmp = "!alive 12345";
        ByteBuffer buffer = ByteBuffer.allocate(tmp.getBytes().length);
        while(true)
        {
          buffer.clear();
          InetSocketAddress proxyaddr = (InetSocketAddress) dchannel.receive(buffer);
          String msg = new String(buffer.array());
          if (msg.length() != tmp.length())
            continue;
          assert msg.matches("!alive 1[0-9]{4}");
          try
          {
            pool.execute(new ProxyConnection(proxyaddr.getHostName(),
                  Integer.parseInt(msg.substring("!alive ".length())), fileservers));
          }
          catch(NumberFormatException e)
          {
            /* simple ignore that packet */
          }
        }
      }
      catch(IOException e)
      {
        /* ignore that exception
         * thread will shutdown and unlock the main thread
         * which will shutdown the application
         */
      }
 
      pool.shutdown();
      try
      {
        if (!pool.awaitTermination(100, TimeUnit.MILLISECONDS))
          out.println("Trying to shutdown the UDP Proxy connections. This may take up to 15 seconds...");
        if (!pool.awaitTermination(5, TimeUnit.SECONDS))
        {
          pool.shutdownNow();
          if (!pool.awaitTermination(5, TimeUnit.SECONDS))
            err.println("Error: UDP Proxy connections did not terminate. You may have to kill that appplication.");
        }
      }
      catch(InterruptedException e)
      {
        pool.shutdownNow();
      }
 
      synchronized(mainLock)
      {
        mainLock.notify();
      }
    }
  }
 
  /*==========================================================================*/
 
  public class ClientConnection
    extends CommandNetwork
    implements Runnable
  {
    private final SocketChannel sock;
    private final FSRecords fileservers;
    private final UserRecords users;
    private final Utils.EncObjectInputStream clin;
    private final Utils.EncObjectOutputStream clout;
    private UserRecord user = null;
    private final String clientaddr;
    HashMap<String, Long> filelist = null;
 
    private CommandNetwork fscmd;
    private ArrayList<SocketChannel> fsschannel;
    private ArrayList<Utils.EncObjectInputStream> fsin;
    private ArrayList<Utils.EncObjectOutputStream> fsout;
    private ArrayList<FSRecord> fileserver;
    private int curconn;
 
    private String mychallenge;
 
    ClientConnection(SocketChannel sock, FSRecords fileservers,
        UserRecords users)
      throws NoSuchMethodException, IOException
    {
      this.sock = sock;
      this.clout = new Utils.EncObjectOutputStream(sock.socket().getOutputStream());
      this.clin  = new Utils.EncObjectInputStream(new BufferedInputStream(sock.socket().getInputStream()));
      this.fileservers  = fileservers;
      this.users        = users;
      this.clientaddr   = "tcp:/" + sock.socket().getInetAddress() + ":" + sock.socket().getPort();
 
      fsschannel = new ArrayList<SocketChannel>();
      fsin  = new ArrayList<Utils.EncObjectInputStream>();
      fsout = new ArrayList<Utils.EncObjectOutputStream>();
      fileserver = new ArrayList<FSRecord>();
 
      clin.setCipher(rsadecrypt);
 
      cmdHandler.register("!login",    this, "cmdLogin");
      cmdHandler.register("!buy",      this, "cmdBuy");
      cmdHandler.register("!credits",  this, "cmdCredits");
      cmdHandler.register("!list",     this, "cmdList");
      cmdHandler.register("!download", this, "cmdDownload");
      cmdHandler.register("!upload",   this, "cmdUpload");
      cmdHandler.register("!upload2",  this, "cmdUpload2");
      cmdHandler.register("unknown",   this, "cmdUnknown");
 
      fscmd = new CommandNetwork();
      fscmd.setOneCommandMode(true);
      fscmd.cmdHandler.register("!error",    this, "cmdFSRelayOutput");
      fscmd.cmdHandler.register("!output",   this, "cmdFSRelayOutput");
      fscmd.cmdHandler.register("!download", this, "cmdFSDownload");
      fscmd.cmdHandler.register("!list",     this, "cmdFSList");
      fscmd.cmdHandler.register("!hasherr",  this, "cmdFSHashError");
      fscmd.cmdHandler.register("unknown",   this, "cmdFSRelayOutput");
    }
 
    /*------------------------------------------------------------------------*/
 
    public boolean checkLogin()
      throws IOException
    {
      if (user == null)
      {
        Utils.sendError(clout, "Not logged in");
        return false;
      }
      return true;
    }
 
    /*------------------------------------------------------------------------*/
 
    public void cmdLogin(String cmd, String[] args)
      throws IOException
    {
      if (user != null)
      {
        Utils.sendError(clout, "Already logged in");
        return;
      }
 
      if (args.length != 2)
      {
        Utils.sendError(clout, "Invalid Syntax: !login <username>");
        return;
      }
 
      String firstMessage = cmd + " " + Utils.join(Arrays.asList(args), " ");
      assert firstMessage.matches("!login \\w+ [" + B64 + "]{43}=") : "1st message";
 
      synchronized(users)
      {
        UserRecord record = users.get(args[0]);
        if (record == null)
        {
          Utils.sendError(clout, "Invalid username");
          return;
        }
 
        user = record;
      }
 
      /* read and init users public key */
      File pemfile = new File(keysDir, args[0] + ".pub.pem");
      if (!pemfile.isFile())
      {
        Utils.sendError(clout, "No public keyfile");
        return;
      }
      if (!pemfile.canRead())
      {
        Utils.sendError(clout, "Your public keyfile is not readable");
        return;
      }
      try
      {
        PEMReader in = new PEMReader(new FileReader(pemfile));
        PublicKey publicKey = (PublicKey) in.readObject();
        rsaencrypt.init(Cipher.ENCRYPT_MODE, publicKey);
        clout.setCipher(rsaencrypt);
      }
      catch(FileNotFoundException e)
      {
        Utils.sendError(clout, "Your public keyfile is not readable");
        return;
      }
      catch(IOException e)
      {
        Utils.sendError(clout, "While reading users public key");
        return;
      }
      catch(InvalidKeyException e)
      {
        Utils.sendError(clout, "invalid public key file: " + e.getMessage());
        return;
      }
 
      String clichallenge = args[1];
 
      /* generates a 32 byte secure random number */
      SecureRandom secureRandom = new SecureRandom();
      byte[] tmp = new byte[32];
      secureRandom.nextBytes(tmp);
      mychallenge = new String(Base64.encode(tmp));
 
      SecretKey seckey = null;
      javax.crypto.spec.IvParameterSpec iv = null;
      try
      {
        /* generate aes key */
        KeyGenerator generator = KeyGenerator.getInstance("AES");
        generator.init(256);
        seckey = generator.generateKey();
 
        /* generate iv */
        byte[] tmpiv = new byte[16];
        secureRandom.nextBytes(tmpiv);
        iv = new javax.crypto.spec.IvParameterSpec(tmpiv);
 
        /* init aes */
        aesencrypt.init(Cipher.ENCRYPT_MODE, seckey, iv);
        aesdecrypt.init(Cipher.DECRYPT_MODE, seckey, iv);
      }
      catch(NoSuchAlgorithmException e)
      {
        err.println("Error: Unable to generate AES key: " + e.getMessage());
        return;
      }
      catch(InvalidKeyException e)
      {
        err.println("Error: invalid AES key: " + e.getMessage());
        return;
      }
      catch(InvalidAlgorithmParameterException e)
      {
        err.println("Error: invalid AES parameters: " + e.getMessage());
        return;
      }
 
      ArrayList<String> msg = new ArrayList<String>();
      msg.add("!ok");
      msg.add(clichallenge);
      msg.add(mychallenge);
      msg.add(new String(Base64.encode(seckey.getEncoded())));
      msg.add(new String(Base64.encode(iv.getIV())));
 
      String secondMessage = Utils.join(msg, " ");
      assert secondMessage.matches("!ok [" + B64 + "]{43}= [" + B64 + "]{43}= [" + B64 + "]{43}= [" + B64 + "]{22}==") : "2nd message";
      clout.writeLine(secondMessage);
      clout.flush();
 
      clout.setCipher(aesencrypt);
      clin.setCipher(aesdecrypt);
    }
 
    /*------------------------------------------------------------------------*/
 
    public void cmdBuy(String cmd, String[] args)
      throws IOException
    {
      if (!checkLogin())
        return;
      if (args.length != 1)
      {
        Utils.sendError(clout, "Invalid Syntax: !buy <credits>");
        return;
      }
 
      int add = 0;
      try
      {
        add = Integer.parseInt(args[0]);
        if (add <= 0)
          throw new NumberFormatException("");
      }
      catch(NumberFormatException e)
      {
        Utils.sendError(clout, "Credits must be numberic and positive");
        return;
      }
 
      synchronized(users)
      {
        if (user.credits > Integer.MAX_VALUE - add)
          Utils.sendError(clout, "You can't buy that much/more credits");
        else
        {
          user.credits += add;
          Utils.sendOutput(clout, "You now have " + user.credits + " credits");
        }
      }
    }
 
    /*------------------------------------------------------------------------*/
 
    public void cmdCredits(String cmd, String[] args)
      throws IOException
    {
      if (!checkLogin())
        return;
      if (args.length != 0)
      {
        Utils.sendError(clout, "Invalid Syntax: !credits");
        return;
      }
 
      synchronized(users)
      {
        Utils.sendOutput(clout, "You have " + user.credits + " credits left");
      }
    }
 
    /*------------------------------------------------------------------------*/
 
    public int connectFileserver(FSRecord fileserver)
      throws IOException
    {
      if (fileserver == null || !fileserver.online)
      {
        Utils.sendError(clout, "Unable to execute command. Fileserver not online");
        return -1;
      }
 
      synchronized(fsschannel)
      {
        int conn = fsschannel.size();
        try
        {
          fsschannel.add(conn, SocketChannel.open(new InetSocketAddress(fileserver.host,
                fileserver.port)));
          fsin.add(conn, new Utils.EncObjectInputStream(new BufferedInputStream(
                fsschannel.get(conn).socket().getInputStream())));
          fsout.add(conn, new Utils.EncObjectOutputStream(fsschannel.get(conn).socket().getOutputStream()));
          this.fileserver.add(conn, fileserver);
 
          fsin.get(conn).setMAC(hmac);
          fsout.get(conn).setMAC(hmac);
        }
        catch(IOException e)
        {
          err.println("Error: Unable to connect to fileserver: " + e.getMessage());
          Utils.sendError(clout, "Unable to connect to fileserver");
 
          synchronized(fileservers)
          {
            fileserver.online = false;
            out.println("Fileserver marked as offline: " + fileserver.host + ":" + fileserver.port);
          }
 
          disconnectFileserver(conn);
          return -1;
        }
        return conn;
      }
    }
 
    /*------------------------------------------------------------------------*/
 
    public void disconnectFileserver(int conn)
    {
      if (conn < 0)
        return;
 
      synchronized(fsschannel)
      {
        try
        {
          if (fsout.get(conn) != null)
            fsout.get(conn).flush();
        }
        catch(IndexOutOfBoundsException e)
        {}
        catch(IOException e)
        {}
 
        try
        {
          if (fsin.get(conn) != null)
            fsin.get(conn).close();
          fsin.set(conn, null);
          for(int i = fsin.size() - 1; i >= 0; --i)
          {
            if (fsin.get(i) != null)
              break;
            fsin.remove(i);
          }
        }
        catch(IndexOutOfBoundsException e)
        {}
        catch(IOException e)
        {}
 
        try
        {
          if (fsout.get(conn) != null)
            fsout.get(conn).close();
          fsout.set(conn, null);
          for(int i = fsout.size() - 1; i >= 0; --i)
          {
            if (fsout.get(i) != null)
              break;
            fsout.remove(i);
          }
        }
        catch(IndexOutOfBoundsException e)
        {}
        catch(IOException e)
        {}
 
        try
        {
          if (fsschannel.get(conn) != null)
            fsschannel.get(conn).close();
          fsschannel.set(conn, null);
          for(int i = fsschannel.size() - 1; i >= 0; --i)
          {
            if (fsschannel.get(i) != null)
              break;
            fsschannel.remove(i);
          }
        }
        catch(IndexOutOfBoundsException e)
        {}
        catch(IOException e)
        {}
 
        try
        {
          fileserver.set(conn, null);
          for(int i = fileserver.size() - 1; i >= 0; --i)
          {
            if (fileserver.get(i) != null)
              break;
            fileserver.remove(i);
          }
        }
        catch(IndexOutOfBoundsException e)
        {}
      }
    }
 
    /*------------------------------------------------------------------------*/
 
    public void cmdList(String cmd, String[] args)
      throws IOException
    {
      if (!checkLogin())
        return;
      if (args.length != 0)
      {
        Utils.sendError(clout, "Invalid Syntax: !list");
        return;
      }
 
      filelist = new HashMap<String, Long>();
      synchronized(fileservers)
      {
        for(FSRecord fileserver : fileservers.values())
        {
          if (!fileserver.online)
            continue;
          int conn = -1;
          try
          {
            if ((conn = connectFileserver(fileserver)) < 0)
              return;
            fsout.get(conn).writeLine(cmd + " " + Utils.join(Arrays.asList(args), " "));
            fsout.get(conn).flush();
            curconn = conn;
            fscmd.run(fsin.get(conn));
          }
          catch(Utils.HashError e)
          {
            fileserver.online = false;
          }
          catch(IOException e)
          {
            fileserver.online = false;
            Utils.sendError(clout, "Connection to fileserver " + fileserver.host + ":" + fileserver.port + " terminated unexpected");
          }
          disconnectFileserver(conn);
        }
      }
 
      if (filelist.size() == 0)
        Utils.sendOutput(clout, "No files available");
      else
      {
        ArrayList<String> tmp = new ArrayList<String>();
        for(Map.Entry<String, Long> file : filelist.entrySet())
          tmp.add(file.getKey() + " v=" + file.getValue());
        Utils.sendOutput(clout, tmp.toArray(new String[tmp.size()]));
      }
 
      clout.flush();
    }
 
    /*------------------------------------------------------------------------*/
 
    public void cmdDownload(String cmd, String[] args)
      throws IOException
    {
      if (!checkLogin())
        return;
      if (args.length != 1)
      {
        Utils.sendError(clout, "Invalid Syntax: !download <filename>");
        return;
      }
 
      FSRecord curfs = null;
      long curversion = -1;
      synchronized(fileservers)
      {
        calcGifford();
        for(FSRecord fileserver : rfileservers)
        {
          filelist = new HashMap<String, Long>();
          int conn = -1;
          try
          {
            if ((conn = connectFileserver(fileserver)) < 0)
              return;
            fsout.get(conn).writeLine("!list");
            fsout.get(conn).flush();
            curconn = conn;
            fscmd.run(fsin.get(conn));
          }
          catch(Utils.HashError e)
          {
            fileserver.online = false;
          }
          catch(IOException e)
          {
            fileserver.online = false;
            Utils.sendError(clout, "Connection to fileserver " + fileserver.host + ":" + fileserver.port + " terminated unexpected");
          }
          disconnectFileserver(conn);
 
          Long version = filelist.get(args[0]);
          if (version != null && version > curversion)
          {
            curfs = fileserver;
            curversion = version;
          }
        }
      }
 
      if (curfs == null)
      {
        Utils.sendError(clout, "File not found or no fileservers online");
        return;
      }
 
      int conn = -1;
      try
      {
        if ((conn = connectFileserver(curfs)) < 0)
          return;
        synchronized(users)
        {
          fsout.get(conn).writeLine(cmd + " " + Utils.join(Arrays.asList(args), " ") + " " + user.credits);
        }
        fsout.get(conn).flush();
        curconn = conn;
        fscmd.run(fsin.get(conn));
      }
      catch(Utils.HashError e)
      {
        curfs.online = false;
      }
      catch(IOException e)
      {
        Utils.sendError(clout, "Connection to fileserver " + fileserver.get(conn).host + ":" + fileserver.get(conn).port + "terminated unexpected");
      }
      clout.flush();
      disconnectFileserver(conn);
    }
 
    /*------------------------------------------------------------------------*/
 
    public void cmdUpload(String cmd, String[] args)
      throws IOException
    {
      if (!checkLogin())
        return;
      if (args.length != 1)
      {
        Utils.sendError(clout, "Invalid Syntax: !upload <filename>");
        return;
      }
 
      synchronized(fileservers)
      {
        if (fileservers.size() == 0)
        {
          Utils.sendError(clout, "Unable to execute command. No fileservers online");
          return;
        }
      }
 
      clout.writeLine(cmd + " " + args[0]);
      clout.flush();
    }
 
    /*------------------------------------------------------------------------*/
 
    public void cmdUpload2(String cmd, String[] args)
      throws IOException
    {
      if (!checkLogin())
        return;
      if (args.length < 2 || args[1].length() <= 0)
      {
        err.println("Error: Invalid " + cmd + "-command paket from client. Ignoring...");
        Utils.sendError(clout, "Internal Error: Invalid packet from client");
        return;
      }
 
      long filesize, filesizecpy;
      if ((filesize = Utils.parseHeaderNum(args, 1)) < 0)
        return;
      filesizecpy = filesize;
 
      long curversion = 0;
      synchronized(fileservers)
      {
        calcGifford();
        for(FSRecord fileserver : rfileservers)
        {
          filelist = new HashMap<String, Long>();
          int conn = -1;
          try
          {
            if ((conn = connectFileserver(fileserver)) < 0)
              return;
            fsout.get(conn).writeLine("!list");
            fsout.get(conn).flush();
            curconn = conn;
            fscmd.run(fsin.get(conn));
          }
          catch(Utils.HashError e)
          {
            fileserver.online = false;
          }
          catch(IOException e)
          {
            fileserver.online = false;
            Utils.sendError(clout, "Connection to fileserver " + fileserver.host + ":" + fileserver.port + " terminated unexpected");
          }
          disconnectFileserver(conn);
 
          Long version = filelist.get(args[0]);
          if (version != null && version > curversion)
            curversion = version;
        }
 
        ArrayList<Integer> conns = new ArrayList<Integer>();
        for(FSRecord fileserver : wfileservers)
        {
          int conn = -1;
          if ((conn = connectFileserver(fileserver)) < 0)
            return;
          conns.add(conn);
        }
 
        try
        {
          for(Integer conn : conns)
            fsout.get(conn).writeLine(cmd + " " + Utils.join(Arrays.asList(args), " ") + " " + (curversion + 1));
 
          byte[] buffer = new byte[8 * 1024];
          int toread = buffer.length;
          while(filesize > 0)
          {
            if (filesize < toread)
              toread = (int) filesize;
            int count = clin.read(buffer, 0, toread);
            if (count == -1)
              throw new IOException("Connection reset by peer");
 
            /* decode + hash that chunk */
            byte[] decbuffer = new byte[aesdecrypt.getOutputSize(count)];
            int deccount = aesdecrypt.update(buffer, 0, count, decbuffer);
            hmac.update(decbuffer, 0, deccount);
 
            for(Integer conn : conns)
              fsout.get(conn).write(decbuffer, 0, deccount);
            filesize -= count;
          }
          /* decryption + hash must be finalized */
          byte[] decbuffer = aesdecrypt.doFinal();
          byte[] hash = hmac.doFinal(decbuffer);
          for(Integer conn : conns)
            fsout.get(conn).write(decbuffer);
          for(Integer conn : conns)
            fsout.get(conn).write(hash);
          for(Integer conn : conns)
            fsout.get(conn).flush();
 
          synchronized(users)
          {
            user.credits += 2 * filesizecpy;
          }
          for(Integer conn : conns)
            fileserver.get(conn).usage += filesizecpy;
          Utils.sendOutput(clout, "File '" + args[0] + "' successfully uploaded.");
        }
        catch(IOException e)
        {
          err.println("Error during file transfer: " + e.getMessage() + ". Closing connection to client");
          stop();
 
          for(Integer conn : conns)
          {
            fileserver.get(conn).usage += filesizecpy;
            fileserver.get(conn).online = false;
            out.println("Fileserver marked as offline: " + fileserver.get(conn).host + ":" + fileserver.get(conn).port);
          }
        }
        catch(GeneralSecurityException e)
        {
          err.println("Error during encrypting file transfer: " + e.getMessage() + ". Closing connection to client");
          stop();
        }
 
        for(Integer conn : conns)
          disconnectFileserver(conn);
      }
    }
 
    /*------------------------------------------------------------------------*/
 
    public void cmdUnknown(String cmd, String[] args)
      throws IOException
    {
      if (args.length == 0 && cmd.equals(mychallenge))
      {
        String thirdMessage = cmd;
        assert thirdMessage.matches("[" + B64 + "]{43}=") : "3rd message";
 
        synchronized(users)
        {
          user.loggedin.add(clientaddr);
        }
 
        Utils.sendOutput(clout, "Successfully logged in");
        return;
      }
 
      err.println("Error: Unknown data from client: " + cmd + " "
          + Utils.join(Arrays.asList(args), " "));
      Utils.sendError(clout, "Unknown command");
    }
 
    /*------------------------------------------------------------------------*/
 
    @SuppressWarnings("deprecation")
    public void cmdFSRelayOutput(String cmd, String[] args)
      throws IOException
    {
      long num;
      if ((num = Utils.parseHeaderNum(args, 0)) < 0)
        return;
      String msg;
      clout.writeLine(cmd + " " + Utils.join(Arrays.asList(args), " "));
      for (; num > 0 && (msg = fsin.get(curconn).readLine()) != null; --num)
        clout.writeLine(msg);
      clout.flush();
    }
 
    /*------------------------------------------------------------------------*/
 
    public void cmdFSDownload(String cmd, String[] args)
      throws IOException
    {
      if (args.length < 2 || args[1].length() <= 0)
      {
        err.println("Error: Invalid " + cmd + "-command paket from fileserver. Ignoring...");
        Utils.sendError(clout, "Internal Error: Invalid packet from fileserver");
        return;
      }
 
      String file = args[0];
      long filesize, filesizecpy;
      if ((filesize = Utils.parseHeaderNum(args, 1)) < 0)
        return;
      filesizecpy = filesize;
 
      clout.writeLine(cmd + " " + Utils.join(Arrays.asList(args), " "));
 
      try
      {
        byte[] buffer = new byte[8 * 1024];
        int toread = buffer.length;
        while(filesize > 0)
        {
          if (filesize < toread)
            toread = (int) filesize;
          int count = fsin.get(curconn).read(buffer, 0, toread);
          if (count == -1)
            throw new IOException("Connection reset by peer");
 
          hmac.update(buffer, 0, count);
 
          /* encode that chunk */
          byte[] encbuffer = new byte[aesencrypt.getOutputSize(count)];
          int enccount = aesencrypt.update(buffer, 0, count, encbuffer);
 
          clout.write(encbuffer, 0, enccount);
          filesize -= count;
        }
 
        /* encryption must be finalized */
        clout.write(aesencrypt.doFinal());
 
        byte[] hashc = hmac.doFinal();
        byte[] hasht = new byte[hmac.getMacLength()];
        fsin.get(curconn).readFully(hasht);
        if (!Arrays.equals(hashc, hasht))
        {
          synchronized(fileservers)
          {
            err.println("Error: invalid MAC during filetransfer. Taking fileserver offline and restarting download.");
            fileserver.get(curconn).online = false;
            out.println("Fileserver marked as offline: " + fileserver.get(curconn).host + ":" + fileserver.get(curconn).port);
            String[] tmp = { file };
            cmdDownload("!download", tmp);
            return;
          }
        }
 
        Utils.sendOutput(clout, "File '" + file + "' successfully downloaded.");
 
        synchronized(users)
        {
          user.credits -= filesizecpy;
        }
 
        synchronized(fileservers)
        {
          fileserver.get(curconn).usage += filesizecpy;
        }
      }
      catch(IOException e)
      {
        err.println("Error during file transfer: " + e.getMessage() + ". Closing connection to client");
        stop();
 
        synchronized(fileservers)
        {
          fileserver.get(curconn).usage += filesizecpy;
          fileserver.get(curconn).online = false;
          out.println("Fileserver marked as offline: " + fileserver.get(curconn).host + ":" + fileserver.get(curconn).port);
        }
      }
      catch(GeneralSecurityException e)
      {
        err.println("Error during encrypting file transfer: " + e.getMessage() + ". Closing connection to client");
        stop();
      }
    }
 
    /*------------------------------------------------------------------------*/
 
    @SuppressWarnings("unchecked")
    public void cmdFSList(String cmd, String[] args)
      throws IOException
    {
      long num;
      if ((num = Utils.parseHeaderNum(args, 0)) < 0)
        return;
 
      String msg;
      for (; num > 0 && (msg = fsin.get(curconn).readLine()) != null; --num)
      {
        int ix = msg.lastIndexOf(' ');
        if (ix == -1)
        {
          err.println("Error: Unknown filelist-message. Ignoring...");
          continue;
        }
 
        try
        {
          String file = msg.substring(0, ix);
          Long fileversion = Long.valueOf(msg.substring(ix + 1));
 
          Long version = filelist.get(file);
          if (version == null || fileversion > version)
            filelist.put(file, fileversion);
        }
        catch(NumberFormatException e)
        {
          err.println("Error: Unable to parse file version. Ignoring...");
          continue;
        }
      }
    }
 
    /*------------------------------------------------------------------------*/
 
    public void cmdFSHashError(String cmd, String[] args)
      throws IOException
    {
      err.println("Error: Hasherror from fileserver!");
      synchronized(fileservers)
      {
        fileserver.get(curconn).online = false;
        out.println("Fileserver marked as offline: " + fileserver.get(curconn).host + ":" + fileserver.get(curconn).port);
      }
      cmdHandler.call2();
    }
 
    /*------------------------------------------------------------------------*/
 
    public void shutdown()
    {
      for(int i = fsschannel.size() - 1; i >= 0; --i)
        disconnectFileserver(i);
 
      try
      {
        clout.flush();
      }
      catch(IOException e)
      {}
 
      try
      {
        clin.close();
      }
      catch(IOException e)
      {}
 
      try
      {
        clout.close();
      }
      catch(IOException e)
      {}
 
      try
      {
        if (sock.isOpen())
          sock.close();
      }
      catch(IOException e)
      {}
    }
 
    /*------------------------------------------------------------------------*/
 
    public void run()
    {
      try
      {
        out.println("[" + Thread.currentThread().getId() + "] New client connection from "
            + clientaddr);
        run(clin);
        clout.flush();
      }
      catch(CommandHandler.Exception e)
      {
        err.println("Internal Error: " + e.getMessage());
        e.printStackTrace();
      }
      catch(IOException e)
      {
        /* ignore that exception
         * it's usually a closed connection from client so
         * we can't do anything about it anyway
         */
      }
 
      if (user != null)
      {
        synchronized(users)
        {
          user.loggedin.remove(user.loggedin.indexOf(clientaddr));
        }
      }
 
      out.println("[" + Thread.currentThread().getId() + "] Connection closed");
      shutdown();
    }
  }
 
  /*==========================================================================*/
 
  public class TCPSocketReader
    implements Runnable
  {
    private final ServerSocketChannel sschannel;
    private final FSRecords fileservers;
    private final UserRecords users;
    private final Object mainLock;
    private final ExecutorService pool;
 
    TCPSocketReader(ServerSocketChannel sschannel, FSRecords fileservers,
        UserRecords users, Object mainLock)
    {
      this.sschannel   = sschannel;
      this.fileservers = fileservers;
      this.users       = users;
      this.mainLock    = mainLock;
      this.pool        = Executors.newCachedThreadPool();
    }
 
    /*------------------------------------------------------------------------*/
 
    public void run()
    {
      try
      {
        while(true)
          pool.execute(new ClientConnection(sschannel.accept(), fileservers, users));
      }
      catch(NoSuchMethodException e)
      {
        err.println("Error: Unable to setup remote command handler");
      }
      catch(IOException e)
      {
        /* ignore that exception
         * thread will shutdown and unlock the main thread
         * which will shutdown the application
         */
      }
 
      pool.shutdown();
      try
      {
        if (!pool.awaitTermination(100, TimeUnit.MILLISECONDS))
          out.println("Trying to shutdown the client connections. This may take up to 15 seconds...");
        if (!pool.awaitTermination(5, TimeUnit.SECONDS))
        {
          pool.shutdownNow();
          if (!pool.awaitTermination(5, TimeUnit.SECONDS))
            err.println("Error: Client connections did not terminate. You may have to kill that appplication.");
        }
      }
      catch(InterruptedException e)
      {
        pool.shutdownNow();
      }
 
      synchronized(mainLock)
      {
        mainLock.notify();
      }
    }
  }
 
  /*==========================================================================*/
 
  public class Interactive
    extends CommandInteractive
    implements Runnable
  {
    private final InputStream sin;
    private final Object mainLock;
 
    Interactive(InputStream sin, Object mainLock)
      throws NoSuchMethodException
    {
      this.sin      = sin;
      this.mainLock = mainLock;
 
      cmdHandler.register("unknown",      this, "cmdUnknown");
      cmdHandler.register("!fileservers", this, "cmdFileservers");
      cmdHandler.register("!users",       this, "cmdUsers");
      cmdHandler.register("!exit",        this, "cmdExit");
    }
 
    /*------------------------------------------------------------------------*/
 
    public void cmdUnknown(String cmd, String[] args)
    {
      err.println("Unknown command: " + cmd + " "
          + Utils.join(Arrays.asList(args), " "));
    }
 
    /*------------------------------------------------------------------------*/
 
    public void cmdFileservers(String cmd, String[] args)
    {
      synchronized(fileservers)
      {
        if (fileservers.size() == 0)
          out.println("No fileservers registered");
        else
        {
          calcGifford();
          int line = 1;
          int pad  = Integer.toString(fileservers.size()).length();
          for(Map.Entry<String, FSRecord> entry : fileservers.entrySet())
          {
            FSRecord record = entry.getValue();
            out.println(String.format("%0" + pad + "d. IP: %s, Port: %d, %s, Usage: %d, Quorum: %s%s",
                  line, record.host, record.port,
                  (record.online) ? "online" : "offline",
                  record.usage,
                  (rfileservers.contains(record)) ? "R" : "",
                  (wfileservers.contains(record)) ? "W" : ""));
            ++line;
          }
        }
      }
    }
 
    /*------------------------------------------------------------------------*/
 
    public void cmdUsers(String cmd, String[] args)
    {
      synchronized(users)
      {
        if (users.size() == 0)
          out.println("No users registered");
        else
        {
          int line = 1;
          int pad  = Integer.toString(users.size()).length();
          for(Map.Entry<String, UserRecord> entry : users.entrySet())
          {
            UserRecord record = entry.getValue();
            out.println(String.format("%0" + pad + "d. User: %s, %s, Credits: %d",
                  line, record.name,
                  (record.loggedin.size() > 0) ? "online" : "offline",
                  record.credits));
            for(String host : record.loggedin)
              out.println(String.format("%1$#" + (pad + 1) + "s Client: %2$s",
                    " ", host));
            ++line;
          }
        }
      }
    }
 
    /*------------------------------------------------------------------------*/
 
    public void cmdExit(String cmd, String[] args)
    {
      stop();
    }
 
    /*------------------------------------------------------------------------*/
 
    public void printPrompt()
    {
      out.print(">: ");
      out.flush();
    }
 
    /*------------------------------------------------------------------------*/
 
    public void run()
    {
      try
      {
        run(sin);
      }
      catch(CommandHandler.Exception e)
      {
        err.println("Internal Error: " + e.getMessage());
      }
      catch (IOException e)
      {
        /* ignore that exception
         * thread will shutdown and unlock the main thread
         * which will shutdown the application
         */
      }
 
      synchronized(mainLock)
      {
        mainLock.notify();
      }
    }
  }
 
  /*==========================================================================*/
 
  public class CheckFSTask
    implements Runnable
  {
    private FSRecords fileservers;
    private final int fserverTimeout;
 
    CheckFSTask(FSRecords fileservers, int fserverTimeout)
    {
      this.fileservers    = fileservers;
      this.fserverTimeout = fserverTimeout;
    }
 
    /*------------------------------------------------------------------------*/
 
    public void run()
    {
      synchronized(fileservers)
      {
        long curTime = Calendar.getInstance().getTimeInMillis();
        for(Map.Entry<String, FSRecord> entry : fileservers.entrySet())
        {
          if (entry.getValue().online && entry.getValue().lastUpdate + fserverTimeout < curTime)
          {
            entry.getValue().online = false;
            out.println("Fileserver has gone offline: " + entry.getKey());
          }
        }
      }
    }
  }
 
  /*==========================================================================*/
 
  private static int tcpPort;
  private static int udpPort;
  private static int fserverTimeout;
  private static int checkPeriod;
  private FSRecords fileservers;
  private ArrayList<FSRecord> rfileservers;
  private ArrayList<FSRecord> wfileservers;
  private UserRecords users;
  private ScheduledExecutorService scheduler = null;
  private DatagramChannel dchannel = null;
  private Thread tUDPSocketReader = null;
  private ServerSocketChannel sschannel = null;
  private Thread tTCPSocketReader = null;
  private Thread tInteractive = null;
  private InputStream stdin = null;
  private final Object mainLock = new Object();
 
  private static String keysDir;
  private static String proxyKey;
  private PrivateKey privateKey = null;
  private Cipher rsaencrypt, rsadecrypt;
  private Cipher aesencrypt, aesdecrypt;
  private Mac hmac;
  private final String B64 = "a-zA-Z0-9/+";
 
  /*--------------------------------------------------------------------------*/
 
  Proxy()
  {
    fileservers = new FSRecords();
    rfileservers = new ArrayList<FSRecord>();
    wfileservers = new ArrayList<FSRecord>();
    users = new UserRecords();
 
    try
    {
      rsaencrypt = Cipher.getInstance("RSA/NONE/OAEPWithSHA256AndMGF1Padding");
      rsadecrypt = Cipher.getInstance("RSA/NONE/OAEPWithSHA256AndMGF1Padding");
      aesencrypt = Cipher.getInstance("AES/CTR/NoPadding");
      aesdecrypt = Cipher.getInstance("AES/CTR/NoPadding");
    }
    catch(NoSuchAlgorithmException e)
    {
      bailout("Unable to initialize cipher: " + e.getMessage());
    }
    catch(NoSuchPaddingException e)
    {
      bailout("Unable to initialize cipher: " + e.getMessage());
    }
  }
 
  /*--------------------------------------------------------------------------*/
 
  public static void usage()
    throws Utils.Shutdown
  {
    out.println("Usage: Proxy\n");
 
    // Java is some piece of crap which doesn't allow me to set exitcode w/o
    // using System.exit. Maybe someday Java will be a fully functional
    // programming language, but I wouldn't bet my money
    //System.exit(1);
    throw new Utils.Shutdown("FUCK YOU JAVA");
  }
 
  /*--------------------------------------------------------------------------*/
 
  public void bailout(String error)
    throws Utils.Shutdown
  {
    err.println("Error: " + error);
    shutdown();
 
    // Java is some piece of crap which doesn't allow me to set exitcode w/o
    // using System.exit. Maybe someday Java will be a fully functional
    // programming language, but I wouldn't bet my money
    //System.exit(2);
    throw new Utils.Shutdown("FUCK YOU JAVA");
  }
 
  /*--------------------------------------------------------------------------*/
 
  public void parseArgs(String[] args)
  {
    if (args.length != 0)
      usage();
  }
 
  /*--------------------------------------------------------------------------*/
 
  public void parseConfig()
  {
    Config config = null;
    try
    {
      config = new Config("proxy");
    }
    catch(MissingResourceException e)
    {
      bailout("configuration file doesn't exist or isn't readable");
    }
 
    String directive = null;
    try
    {
      directive = "tcp.port";
      tcpPort = config.getInt(directive);
      if (tcpPort <= 0 || tcpPort > 65536)
        bailout("configuration directive '" + directive + "' must be a valid port number (1 - 65535)");
 
      directive = "udp.port";
      udpPort = config.getInt(directive);
      if (udpPort <= 0 || udpPort > 65536)
        bailout("configuration directive '" + directive + "' must be a valid port number (1 - 65535)");
 
      directive = "fileserver.timeout";
      fserverTimeout = config.getInt(directive);
      if (fserverTimeout <= 0)
        bailout("configuration directive '" + directive + "' must be a positive number");
 
      directive = "fileserver.checkPeriod";
      checkPeriod = config.getInt(directive);
      if (checkPeriod <= 0)
        bailout("configuration directive '" + directive + "' must be a positive number");
 
      directive = "keys.dir";
      keysDir = config.getString(directive);
      File dir = new File(keysDir);
      if (!dir.isDirectory())
        bailout("configuration directive '" + directive + "' is not a directory");
      if (!dir.canRead())
        bailout("configuration directive '" + directive + "' is not readable");
 
      directive = "key";
      proxyKey = config.getString(directive);
      File key = new File(proxyKey);
      if (!key.isFile())
        bailout("configuration directive '" + directive + "' is not a file");
      if (!key.canRead())
        bailout("configuration directive '" + directive + "' is not readable");
      PEMReader in = new PEMReader(new FileReader(key), new PasswordFinder()
          {
            @Override
            public char[] getPassword()
            {
              try
              {
                /* reads the password from standard input for decrypting the private key */
                out.println("Enter pass phrase for proxy key:");
                return new BufferedReader(new InputStreamReader(System.in)).readLine().toCharArray();
              }
              catch(IOException e)
              {
                char[] tmp = {};
                return tmp;
              }
            }
          });
      try
      {
        KeyPair keyPair = (KeyPair) in.readObject();
        privateKey = keyPair.getPrivate();
        rsadecrypt.init(Cipher.DECRYPT_MODE, privateKey);
      }
      catch(IOException e)
      {
        bailout("Error while reading private key of proxy. Maybe wrong pass phrase");
      }
 
      directive = "hmac.key";
      File hmackey = new File(config.getString(directive));
      if (!hmackey.isFile())
        bailout("configuration directive '" + directive + "' is not a file");
      if (!hmackey.canRead())
        bailout("configuration directive '" + directive + "' is not readable");
 
      byte[] keybytes = new byte[1024];
      FileInputStream fis = new FileInputStream(hmackey);
      fis.read(keybytes);
      fis.close();
 
      hmac = Mac.getInstance("HmacSHA256");
      hmac.init(new javax.crypto.spec.SecretKeySpec(Hex.decode(keybytes), "HmacSHA256"));
    }
    catch(FileNotFoundException e)
    {
      bailout("unable to read file of directive '" + directive + "'");
    }
    catch(IOException e)
    {
      bailout("Error while reading file: " + e.getMessage());
    }
    catch(InvalidKeyException e)
    {
      bailout("invalid key file: " + e.getMessage());
    }
    catch(MissingResourceException e)
    {
      bailout("configuration directive '" + directive + "' is not set");
    }
    catch(NumberFormatException e)
    {
      bailout("configuration directive '" + directive + "' must be numeric");
    }
    catch(NoSuchAlgorithmException e)
    {
      bailout("Unable to initialize cipher: " + e.getMessage());
    }
  }
 
  /*--------------------------------------------------------------------------*/
 
  public void parseUsers()
  {
    Config config = null;
    try
    {
      config = new Config("user");
    }
    catch(MissingResourceException e)
    {
      bailout("Unable to read from user properties file: " + e.getMessage());
    }
 
    /* first load the users only */
    for (Enumeration e = config.getKeys(); e.hasMoreElements();)
    {
      String prop = (String)e.nextElement();
      String[] pieces = prop.split("\\.", 2);
      String user = pieces[0];
 
      if (pieces.length == 1)
      {
        int credits = config.getInt(prop);
        if (credits < 0)
        {
          err.println("Property " + prop + " must be positive number. Skipping...");
          continue;
        }
        users.put(user, new UserRecord(user, credits));
      }
    }
 
    /* next load their properties */
    for (Enumeration e = config.getKeys(); e.hasMoreElements();)
    {
      String prop = (String)e.nextElement();
      String[] pieces = prop.split("\\.", 2);
      String user = pieces[0];
 
      if (pieces.length == 1)
        continue;
      else if (pieces.length == 2)
      {
        UserRecord record = users.get(user);
        if (record == null)
        {
          err.println("Can't load user properties for unknown user '" + user + "'. Skipping...");
          continue;
        }
 
        if (pieces[1].equals("password"))
          record.pass = config.getString(prop);
        else
          err.println("Property " + prop + " is unknown. Skipping...");
      }
      else
        err.println("Property " + prop + " is unknown. Skipping...");
    }
  }
 
  /*--------------------------------------------------------------------------*/
 
  public void calcGifford()
  {
    synchronized(fileservers)
    {
      rfileservers.clear();
      wfileservers.clear();
 
      ArrayList<FSRecord> fslist = new ArrayList<FSRecord>();
      for(FSRecord record : fileservers.values())
      {
        if (!record.online)
          continue;
        fslist.add(record);
      }
      if (fslist.size() == 0)
        return;
      Collections.sort(fslist);
 
      int numwrite = (int) Math.floor(fslist.size() / 2) + 1;
      int numread = fslist.size() - numwrite + 1;
 
      for(int i = 0; i < numread; ++i)
        rfileservers.add(fslist.get(i));
      for(int i = 0; i < numwrite; ++i)
        wfileservers.add(fslist.get(i));
    }
  }
 
  /*--------------------------------------------------------------------------*/
 
  public void shutdown()
  {
    try
    {
      if (scheduler != null)
      {
        scheduler.shutdownNow();
        scheduler.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
      }
    }
    catch(InterruptedException e)
    {}
 
    try
    {
      if (dchannel != null)
        dchannel.close();
    }
    catch(IOException e)
    {}
 
    try
    {
      if (tUDPSocketReader != null)
        tUDPSocketReader.join();
    }
    catch(InterruptedException e)
    {}
 
    try
    {
      if (sschannel != null)
        sschannel.close();
    }
    catch(IOException e)
    {}
 
    try
    {
      if (tTCPSocketReader != null)
        tTCPSocketReader.join();
    }
    catch(InterruptedException e)
    {}
 
    try
    {
      if (tInteractive != null)
      {
        tInteractive.interrupt();
        tInteractive.join();
      }
    }
    catch(InterruptedException e)
    {}
 
    try
    {
      if (stdin != null)
        stdin.close();
    }
    catch(IOException e)
    {}
  }
 
  /*--------------------------------------------------------------------------*/
 
  public void run(String[] args)
  {
    parseArgs(args);
    parseConfig();
 
    synchronized(users)
    {
      parseUsers();
      out.println("Users loaded successfully: " + users.size() + " users loaded");
    }
 
    synchronized(mainLock)
    {
      scheduler = Executors.newScheduledThreadPool(1);
      ScheduledFuture<?> checkFSTimer = scheduler.scheduleAtFixedRate(
          new CheckFSTask(fileservers, fserverTimeout),
          0, checkPeriod, TimeUnit.MILLISECONDS);
 
      try
      {
        dchannel = DatagramChannel.open();
        dchannel.socket().bind(new InetSocketAddress(udpPort));
        tUDPSocketReader = new Thread(new UDPSocketReader(dchannel, fileservers,
              mainLock));
        tUDPSocketReader.start();
        out.println("Listening on udp:/" + dchannel.socket().getLocalSocketAddress());
      }
      catch(IOException e)
      {
        bailout("Unable to create UDP Socket: " + e.getMessage());
      }
 
      try
      {
        sschannel = ServerSocketChannel.open();
        sschannel.socket().bind(new InetSocketAddress(tcpPort));
        tTCPSocketReader = new Thread(new TCPSocketReader(sschannel,
              fileservers, users, mainLock));
        tTCPSocketReader.start();
        out.println("Listening on tcp:/" + sschannel.socket().getLocalSocketAddress());
      }
      catch(IOException e)
      {
        bailout("Unable to create TCP Socket: " + e.getMessage());
      }
 
      try
      {
        InputStream stdin = java.nio.channels.Channels.newInputStream(
            new FileInputStream(FileDescriptor.in).getChannel());
        tInteractive = new Thread(new Interactive(stdin, mainLock));
        tInteractive.start();
      }
      catch(NoSuchMethodException e)
      {
        bailout("Unable to setup interactive command handler");
      }
 
      out.println("Proxy startup successful!");
      try
      {
        mainLock.wait();
      }
      catch(InterruptedException e)
      {
        /* if we get interrupted -> ignore */
      }
 
      try
      {
        /* let the threads shutdown */
        Thread.sleep(100);
      }
      catch(InterruptedException e)
      {}
    }
 
    if (tUDPSocketReader != null && !tUDPSocketReader.isAlive())
      bailout("Listening UDP socket closed unexpected. Terminating...");
    if (tTCPSocketReader != null && !tTCPSocketReader.isAlive())
      bailout("Listening TCP socket closed unexpected. Terminating...");
 
    shutdown();
  }
 
  /*--------------------------------------------------------------------------*/
 
  public static void main(String[] args)
  {
    try
    {
      Proxy proxy = new Proxy();
      proxy.run(args);
    }
    catch(Utils.Shutdown e)
    {}
  }
}