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 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.Collections;
import java.util.Properties;
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 final String pass;
    public int credits;
    public ArrayList<String> loggedin;
 
    UserRecord(String name, String pass)
    {
      this.name = name;
      this.pass = pass;
      credits   = 0;
      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
      {
        ByteBuffer buffer = ByteBuffer.allocate(4);
        while(true)
        {
          buffer.clear();
          InetSocketAddress proxyaddr = (InetSocketAddress) dchannel.receive(buffer);
          try
          {
            pool.execute(new ProxyConnection(proxyaddr.getHostName(),
                  buffer.getInt(0), fileservers));
          }
          catch(IndexOutOfBoundsException 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 ObjectInputStream clin;
    private final ObjectOutputStream clout;
    private UserRecord user = null;
    private final String clientaddr;
 
    private CommandNetwork fscmd;
    private FSRecord fileserver      = null;
    private SocketChannel fsschannel = null;
    private ObjectInputStream fsin   = null;
    private ObjectOutputStream fsout = null;
 
    ClientConnection(SocketChannel sock, FSRecords fileservers,
        UserRecords users)
      throws NoSuchMethodException, IOException
    {
      this.sock = sock;
      this.clout = new ObjectOutputStream(sock.socket().getOutputStream());
      this.clin  = new ObjectInputStream(new BufferedInputStream(sock.socket().getInputStream()));
      this.fileservers = fileservers;
      this.users       = users;
      this.clientaddr  = "tcp:/" + sock.socket().getInetAddress() + ":" + sock.socket().getPort();
 
      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("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("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> <password>");
        return;
      }
 
      synchronized(users)
      {
        UserRecord record = users.get(args[0]);
        if (record == null || !record.pass.equals(args[1]))
        {
          Utils.sendError(clout, "Invalid username or password");
          return;
        }
 
        user = record;
        user.loggedin.add(clientaddr);
      }
 
      Utils.sendOutput(clout, "Successfully logged in");
    }
 
    /*------------------------------------------------------------------------*/
 
    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 FSRecord getFileserver()
    {
      synchronized(fileservers)
      {
        ArrayList<FSRecord> fslist = new ArrayList<FSRecord>();
        for(FSRecord record : fileservers.values())
        {
          if (!record.online)
            continue;
          fslist.add(record);
        }
        if (fslist.size() == 0)
          return null;
        Collections.sort(fslist);
        return fslist.get(0);
      }
    }
 
    /*------------------------------------------------------------------------*/
 
    public boolean connectFileserver()
      throws IOException
    {
      disconnectFileserver();
 
      fileserver = getFileserver();
      if (fileserver == null)
      {
        Utils.sendError(clout, "Unable to execute command. No fileservers online");
        return false;
      }
 
      try
      {
        fsschannel = SocketChannel.open(new InetSocketAddress(fileserver.host,
              fileserver.port));
        fsin = new ObjectInputStream(new BufferedInputStream(
              fsschannel.socket().getInputStream()));
        fsout = new ObjectOutputStream(fsschannel.socket().getOutputStream());
      }
      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();
        return false;
      }
 
      return true;
    }
 
    /*------------------------------------------------------------------------*/
 
    public void disconnectFileserver()
    {
      try
      {
        if (fsin != null)
          fsout.flush();
      }
      catch(IOException e)
      {}
 
      try
      {
        if (fsin != null)
          fsin.close();
      }
      catch(IOException e)
      {}
 
      try
      {
        if (fsout != null)
          fsout.close();
      }
      catch(IOException e)
      {}
 
      try
      {
        if (fsschannel != null)
          fsschannel.close();
      }
      catch(IOException e)
      {}
 
      fsin  = null;
      fsout = null;
      fileserver = null;
    }
 
    /*------------------------------------------------------------------------*/
 
    public void cmdList(String cmd, String[] args)
      throws IOException
    {
      if (!checkLogin())
        return;
      if (args.length != 0)
      {
        Utils.sendError(clout, "Invalid Syntax: !list");
        return;
      }
 
      try
      {
        if (!connectFileserver())
          return;
        fsout.writeUTF(cmd + " " + Utils.join(Arrays.asList(args), " "));
        fsout.flush();
        fscmd.run(fsin);
      }
      catch(IOException e)
      {
        Utils.sendError(clout, "Connection to fileserver terminated unexpected");
      }
      clout.flush();
      disconnectFileserver();
    }
 
    /*------------------------------------------------------------------------*/
 
    public void cmdDownload(String cmd, String[] args)
      throws IOException
    {
      if (!checkLogin())
        return;
      if (args.length != 1)
      {
        Utils.sendError(clout, "Invalid Syntax: !download <filename>");
        return;
      }
 
      try
      {
        if (!connectFileserver())
          return;
        synchronized(users)
        {
          fsout.writeUTF(cmd + " " + Utils.join(Arrays.asList(args), " ") + " " + user.credits);
        }
        fsout.flush();
        fscmd.run(fsin);
      }
      catch(IOException e)
      {
        Utils.sendError(clout, "Connection to fileserver terminated unexpected");
      }
      clout.flush();
      disconnectFileserver();
    }
 
    /*------------------------------------------------------------------------*/
 
    public void cmdUnknown(String cmd, String[] args)
      throws IOException
    {
      err.println("Error: Unknown data from client: " + cmd + " "
          + Utils.join(Arrays.asList(args), " "));
      Utils.sendError(clout, "Unknown command");
    }
 
    /*------------------------------------------------------------------------*/
 
    public void cmdFSRelayOutput(String cmd, String[] args)
      throws IOException
    {
      long num;
      if ((num = Utils.parseHeaderNum(args, 0)) < 0)
        return;
      String msg;
      clout.writeUTF(cmd + " " + Utils.join(Arrays.asList(args), " "));
      for (; num > 0 && (msg = fsin.readUTF()) != null; --num)
        clout.writeUTF(msg);
    }
 
    /*------------------------------------------------------------------------*/
 
    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;
      }
 
      long filesize, filesizecpy;
      if ((filesize = Utils.parseHeaderNum(args, 1)) < 0)
        return;
      filesizecpy = filesize;
 
      clout.writeUTF(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.read(buffer, 0, toread);
          if (count == -1)
            throw new IOException("Connection reset by peer");
          clout.write(buffer, 0, count);
          filesize -= count;
        }
 
        synchronized(users)
        {
          user.credits -= filesizecpy;
        }
 
        synchronized(fileservers)
        {
          fileserver.usage += filesizecpy;
        }
      }
      catch(IOException e)
      {
        err.println("Error during file transfer: " + e.getMessage() + ". Closing connection to client");
        stop();
 
        synchronized(fileservers)
        {
          fileserver.usage += filesizecpy;
          fileserver.online = false;
          out.println("Fileserver marked as offline: " + fileserver.host + ":" + fileserver.port);
        }
      }
    }
 
    /*------------------------------------------------------------------------*/
 
 
    public void shutdown()
    {
      disconnectFileserver();
 
      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
        {
          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",
                  line, record.host, record.port,
                  (record.online) ? "online" : "offline",
                  record.usage));
            ++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 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();
 
  /*--------------------------------------------------------------------------*/
 
  Proxy()
  {
    fileservers = new FSRecords();
    users = new UserRecords();
  }
 
  /*--------------------------------------------------------------------------*/
 
  public static void usage()
    throws Utils.Shutdown
  {
    out.println("Usage: Proxy tcpPort udpPort fserverTimeout checkPeriod\n");
    out.println("tcpPort\t\t...the port to be used for instantiating a ServerSocket");
    out.println("udpPort\t\t...the port to be used for instantiating a DatagramSocket");
    out.println("fserverTimeout\t...the period in milliseconds each fileserver has to send an isAlive packet");
    out.println("\t\t   if no such packet is received within this time, the fileserver is assumed");
    out.println("\t\t   to be offline and is no longer available for handling requests");
    out.println("checkPeriod\t...specifies that the test whether a fileserver has timed-out or not");
    out.println("\t\t   (see fileserverTimeout). is repeated every checkPeriod milliseconds");
 
    // 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 != 4)
      usage();
 
    try
    {
      tcpPort = Integer.parseInt(args[0]);
      if (tcpPort <= 0 || tcpPort > 65536)
        bailout("tcpPort must be a valid port number (1 - 65535)");
    }
    catch(NumberFormatException e)
    {
      bailout("tcpPort must be numeric");
    }
 
    try
    {
      udpPort = Integer.parseInt(args[1]);
      if (udpPort <= 0 || udpPort > 65536)
        bailout("udpPort must be a valid port number (1 - 65535)");
    }
    catch(NumberFormatException e)
    {
      bailout("udpPort must be numeric");
    }
 
    try
    {
      fserverTimeout = Integer.parseInt(args[2]);
      if (fserverTimeout <= 0)
        bailout("fserverTimeout must be positive");
    }
    catch(NumberFormatException e)
    {
      bailout("fserverTimeout must be numeric");
    }
 
    try
    {
      checkPeriod = Integer.parseInt(args[3]);
      if (checkPeriod <= 0)
        bailout("checkPeriod must be positive");
    }
    catch(NumberFormatException e)
    {
      bailout("checkPeriod must be numeric");
    }
  }
 
  /*--------------------------------------------------------------------------*/
 
  public void parseUsers(InputStream in)
    throws IOException, IllegalArgumentException
  {
    if (in == null)
      throw new IOException("Properties file doesn't exist");
    Properties props = new Properties();
    props.load(in);
    for(String prop : props.stringPropertyNames())
    {
      String[] pieces = prop.split("\\.", 2);
      String user = pieces[0];
 
      if (pieces.length == 1)
        users.put(user, new UserRecord(user, props.getProperty(prop)));
      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("credits"))
        {
          try
          {
            int credits = Integer.parseInt(props.getProperty(prop));
            if (credits < 0)
            {
              err.println("Property " + prop + " must be positive number. Skipping...");
              continue;
            }
            record.credits = credits;
          }
          catch(NumberFormatException e)
          {
            err.println("Property " + prop + " must be numeric. Skipping...");
          }
        }
        else
          err.println("Property " + prop + " is unknown. Skipping...");
      }
      else
        err.println("Property " + prop + " is unknown. Skipping...");
    }
  }
 
  /*--------------------------------------------------------------------------*/
 
  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);
 
    synchronized(users)
    {
      try
      {
        InputStream in = ClassLoader.getSystemResourceAsStream("user.properties");
        if (in == null)
          bailout("Properties file doesn't exist or isn't readable");
        parseUsers(in);
        in.close();
        out.println("Users loaded successfully");
      }
      catch(IOException e)
      {
        bailout("Unable to read from properties file: " + e.getMessage());
      }
      catch(IllegalArgumentException e)
      {
        bailout("Malformed properties file: " + e.getMessage());
      }
    }
 
    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)
    {}
  }
}