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.util.Arrays;
import java.util.HashMap;
import java.util.MissingResourceException;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
 
import javax.crypto.Mac;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import org.bouncycastle.util.encoders.Hex;
import org.bouncycastle.util.encoders.Base64;
 
import java.net.*;
import java.io.*;
 
/*
 * Fileserver 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 Fileserver
{
  public class ProxyConnection
    extends CommandNetwork
    implements Runnable
  {
    private final SocketChannel sock;
    private final String sharedFolder;
    private final Utils.EncObjectInputStream oin;
    private final Utils.EncObjectOutputStream oout;
 
    ProxyConnection(SocketChannel sock, String sharedFolder)
      throws NoSuchMethodException, IOException
    {
      this.sock = sock;
      this.oout = new Utils.EncObjectOutputStream(sock.socket().getOutputStream());
      this.oin  = new Utils.EncObjectInputStream(new BufferedInputStream(sock.socket().getInputStream()));
      this.sharedFolder = sharedFolder;
      this.oout.setMAC(hmac);
      this.oin.setMAC(hmac);
 
      setOneCommandMode(true);
      cmdHandler.register("!list",     this, "cmdList");
      cmdHandler.register("!download", this, "cmdDownload");
      cmdHandler.register("!upload2",  this, "cmdUpload2");
      cmdHandler.register("unknown",   this, "cmdUnknown");
    }
 
    /*------------------------------------------------------------------------*/
 
    public void cmdList(String cmd, String[] args)
      throws IOException
    {
      if (args.length != 0)
      {
        err.println("Error: Invalid " + cmd + "-command paket from proxy. Ignoring...");
        return;
      }
 
      oout.writeLine(cmd + " " + filelist.size());
      for(java.util.Map.Entry<String, Long> file : filelist.entrySet())
        oout.writeLine(file.getKey() + " " + file.getValue().toString());
      oout.flush();
    }
 
    /*------------------------------------------------------------------------*/
 
    public void cmdDownload(String cmd, String[] args)
      throws IOException
    {
      if (args.length < 2 || args[1].length() <= 0)
      {
        err.println("Error: Invalid " + cmd + "-command paket from proxy. Ignoring...");
        return;
      }
 
      long credits;
      if ((credits = Utils.parseHeaderNum(args, 1)) < 0)
        return;
 
      File file = new File(sharedFolder, args[0]);
      if (!file.exists() || !file.getParent().equals(sharedFolder))
      {
        Utils.sendError(oout, "File '" + args[0] + "' does not exist");
        return;
      }
      if (!file.isFile())
      {
        Utils.sendError(oout, "File '" + args[0] + "' is not a file");
        return;
      }
      if (!file.canRead())
      {
        Utils.sendError(oout, "File '" + args[0] + "' is not readable");
        return;
      }
 
      long filesize = file.length();
      if (credits < filesize)
      {
        Utils.sendOutput(oout, "You don't have enough credits");
        return;
      }
 
      try
      {
        FileInputStream fin = new FileInputStream(file);
 
        oout.writeLine(cmd + " " + file.getName() + " " + filesize);
 
        byte[] buffer = new byte[8 * 1024];
        int toread = buffer.length;
        while(filesize > 0)
        {
          if (filesize < toread)
            toread = (int) filesize;
          int count = fin.read(buffer, 0, toread);
          if (count == -1)
            throw new IOException("Error while reading from file");
          hmac.update(buffer, 0, count);
          oout.write(buffer, 0, count);
          filesize -= count;
        }
        oout.write(hmac.doFinal());
      }
      catch(FileNotFoundException e)
      {
        Utils.sendError(oout, "File '" + args[0] + "' is not readable");
      }
      catch(IOException e)
      {
        err.println("Error during file transfer: " + e.getMessage());
      }
    }
 
    /*------------------------------------------------------------------------*/
 
    public void cmdUpload2(String cmd, String[] args)
      throws IOException
    {
      if (args.length < 3 || args[1].length() <= 0)
      {
        err.println("Error: Invalid " + cmd + "-command paket from proxy. Ignoring...");
        return;
      }
 
      long filesize;
      if ((filesize = Utils.parseHeaderNum(args, 1)) < 0)
        return;
 
      long version;
      if ((version = Utils.parseHeaderNum(args, 2)) < 0)
        return;
 
      File file = new File(sharedFolder, args[0]);
      file.delete();
      try
      {
        FileOutputStream fout = null;
        try
        {
          fout = new FileOutputStream(file);
        }
        catch(FileNotFoundException e)
        {
          err.println("Error: Unable to write to file '" + file + "': " + e.getMessage());
        }
 
        byte[] buffer = new byte[8 * 1024];
        int toread = buffer.length;
        while(filesize > 0)
        {
          if (filesize < toread)
            toread = (int) filesize;
          int count = oin.read(buffer, 0, toread);
          if (count == -1)
            throw new IOException("Connection reset by peer");
          if (fout != null)
          {
            hmac.update(buffer, 0, count);
            fout.write(buffer, 0, count);
          }
          filesize -= count;
        }
        byte[] hasht = new byte[hmac.getMacLength()];
        oin.readFully(hasht);
 
        if (fout != null)
        {
          byte[] hashc = hmac.doFinal();
          //TODO
          if (!Arrays.equals(hashc, hasht))
          {
            err.println("HASH NOT EQUAL: File may be corrupt");
            filelist.remove(file.getName());
          }
          else
            filelist.put(file.getName(), version);
        }
      }
      catch(IOException e)
      {
        err.println("Error: Error during file transfer: " + e.getMessage());
      }
    }
 
    /*------------------------------------------------------------------------*/
 
    public void cmdUnknown(String cmd, String[] args)
      throws IOException
    {
      err.println("Error: Unknown data from proxy: " + cmd + " "
          + Utils.join(Arrays.asList(args), " "));
      Utils.sendError(oout, "Unknown command");
    }
 
    /*------------------------------------------------------------------------*/
 
    public void hashError()
      throws IOException
    {
      oout.writeLine("!hasherr");
    }
 
    /*------------------------------------------------------------------------*/
 
    public void shutdown()
    {
      try
      {
        oout.flush();
      }
      catch(IOException e)
      {}
 
      try
      {
        oin.close();
      }
      catch(IOException e)
      {}
 
      try
      {
        oout.close();
      }
      catch(IOException e)
      {}
 
      try
      {
        if (sock.isOpen())
          sock.close();
      }
      catch(IOException e)
      {}
    }
 
    /*------------------------------------------------------------------------*/
 
    public void run()
    {
      try
      {
        out.println("[" + Thread.currentThread().getId() + "] New connection from tcp:/"
            + sock.socket().getInetAddress() + ":" + sock.socket().getPort());
        try
        {
          run(oin);
        }
        catch(Utils.HashError e)
        {
          hashError();
        }
        oout.flush();
      }
      catch(CommandHandler.Exception e)
      {
        err.println("Internal Error: " + e.getMessage());
      }
      catch(IOException e)
      {
        /* ignore that exception
         * it's usually a closed connection from client so
         * we can't do anything about it anyway
         */
      }
 
      out.println("[" + Thread.currentThread().getId() + "] Connection closed");
      shutdown();
    }
  }
 
  /*==========================================================================*/
 
  public class TCPSocketReader
    implements Runnable
  {
    private final ServerSocketChannel sschannel;
    private final String sharedFolder;
    private final Object mainLock;
    private final ExecutorService pool;
 
    TCPSocketReader(ServerSocketChannel sschannel, String sharedFolder,
        Object mainLock)
    {
      this.sschannel    = sschannel;
      this.sharedFolder = sharedFolder;
      this.mainLock     = mainLock;
      this.pool         = Executors.newCachedThreadPool();
    }
 
    /*------------------------------------------------------------------------*/
 
    public void run()
    {
      try
      {
        while(true)
          pool.execute(new ProxyConnection(sschannel.accept(), sharedFolder));
      }
      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 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: Proxy 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("!exit",   this, "cmdExit");
    }
 
    /*------------------------------------------------------------------------*/
 
    public void cmdUnknown(String cmd, String[] args)
    {
      err.println("Unknown command: " + cmd + " "
          + Utils.join(Arrays.asList(args), " "));
    }
 
    /*------------------------------------------------------------------------*/
 
    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 PingTask
    implements Runnable
  {
    private final DatagramSocket sock;
    private final DatagramPacket packet;
    private final Object mainLock;
 
    PingTask(DatagramSocket sock, DatagramPacket packet, Object mainLock)
    {
      this.sock     = sock;
      this.packet   = packet;
      this.mainLock = mainLock;
    }
 
    /*------------------------------------------------------------------------*/
 
    public void run()
    {
      try
      {
        sock.send(packet);
      }
      catch(IOException e)
      {
        err.println("Error while sending UDP ping packet: " + e.getMessage()
            + ". Terminating...");
        synchronized(mainLock)
        {
          mainLock.notify();
        }
      }
    }
  }
 
  /*==========================================================================*/
 
  private static String sharedFolder;
  private static String proxyHost;
  private static int tcpPort;
  private static int proxyUDPPort;
  private static int alivePeriod;
  private ScheduledExecutorService scheduler = null;
  private ServerSocketChannel sschannel = null;
  private DatagramSocket dsock = null;
  private Thread tTCPSocketReader = null;
  private Thread tInteractive = null;
  private InputStream stdin = null;
  private final Object mainLock = new Object();
  private HashMap<String, Long> filelist;
 
  private Mac hmac;
  private final String B64 = "a-zA-Z0-9/+";
 
  /*--------------------------------------------------------------------------*/
 
  public Fileserver()
  {
    filelist = new HashMap<String, Long>();
  }
 
  /*--------------------------------------------------------------------------*/
 
  public static void usage()
    throws Utils.Shutdown
  {
    out.println("Usage: Fileserver sharedFolder tcpPort\n");
    out.println("sharedFolder\t...the directory that contains all the files clients can download or have uploaded");
    out.println("tcpPort\t\t...the port to be used for instantiating a ServerSocket");
 
    // 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 != 2)
      usage();
 
    sharedFolder = args[0];
    File sharedir = new File(sharedFolder);
    if (!sharedir.isDirectory())
      bailout("sharedFolder '" + sharedFolder + "' is not a directory");
    if (!sharedir.canRead())
      bailout("sharedFolder '" + sharedFolder + "' is not readable");
 
    try
    {
      tcpPort = Integer.parseInt(args[1]);
      if (tcpPort <= 0 || tcpPort > 65536)
        bailout("tcpPort must be a valid port number (1 - 65535)");
    }
    catch(NumberFormatException e)
    {
      bailout("tcpPort must be numeric");
    }
  }
 
  /*--------------------------------------------------------------------------*/
 
  public void parseConfig()
  {
    Config config = null;
    try
    {
      config = new Config("fileserver");
    }
    catch(MissingResourceException e)
    {
      bailout("configuration file doesn't exist or isn't readable");
    }
 
    String directive = null;
    try
    {
      directive = "proxy.host";
      proxyHost = config.getString(directive);
      if (proxyHost.length() == 0)
        bailout("configuration directive '" + directive + "' is empty or invalid");
 
      directive = "proxy.udp.port";
      proxyUDPPort = config.getInt(directive);
      if (proxyUDPPort <= 0 || proxyUDPPort > 65536)
        bailout("configuration directive '" + directive + "' must be a valid port number (1 - 65535)");
 
      directive = "fileserver.alive";
      alivePeriod = config.getInt(directive);
      if (alivePeriod <= 0)
        bailout("configuration directive '" + directive + "' must be a positive number");
 
      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(MissingResourceException e)
    {
      bailout("configuration directive '" + directive + "' is not set");
    }
    catch(NumberFormatException e)
    {
      bailout("configuration directive '" + directive + "' must be numeric");
    }
    catch(FileNotFoundException e)
    {
      bailout("unable to read file of directive '" + directive + "'");
    }
    catch(NoSuchAlgorithmException e)
    {
      bailout("Unable to initialize cipher: " + e.getMessage());
    }
    catch(InvalidKeyException e)
    {
      bailout("invalid key file: " + e.getMessage());
    }
    catch(IOException e)
    {
      bailout("Error while reading file: " + e.getMessage());
    }
  }
 
  /*--------------------------------------------------------------------------*/
 
  public void readFiles()
  {
    File file = new File(sharedFolder);
    FilenameFilter filter = new FilenameFilter()
    {
      public boolean accept(File dir, String name)
      {
        File file = new File(dir, name);
        return (file.isFile() && file.canRead());
      }
    };
    for(String filename : file.list(filter))
    {
      if (!filelist.containsKey(filename))
        filelist.put(filename, Long.valueOf(0));
    }
  }
 
  /*--------------------------------------------------------------------------*/
 
  public void shutdown()
  {
    try
    {
      if (scheduler != null)
      {
        scheduler.shutdownNow();
        scheduler.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
      }
    }
    catch(InterruptedException e)
    {}
 
    if (dsock != null)
      dsock.close();
 
    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();
    readFiles();
 
    synchronized(mainLock)
    {
      try
      {
        dsock = new DatagramSocket();
        InetAddress proxyaddr = InetAddress.getByName(proxyHost);
        String msg = "!alive " + tcpPort;
        DatagramPacket dpacket = new DatagramPacket(msg.getBytes(),
            msg.getBytes().length, proxyaddr, proxyUDPPort);
        assert new String(dpacket.getData()).matches("!alive 1[0-9]{4}");
 
        scheduler = Executors.newScheduledThreadPool(1);
        ScheduledFuture<?> pingTimer = scheduler.scheduleAtFixedRate(
            new PingTask(dsock, dpacket, mainLock),
            0, alivePeriod, TimeUnit.MILLISECONDS);
      }
      catch(SocketException e)
      {
        bailout("Unable to create UDP Socket: " + e.getMessage());
      }
      catch(UnknownHostException e)
      {
        bailout("Unable to resolve hostname: " + e.getMessage());
      }
 
      try
      {
        sschannel = ServerSocketChannel.open();
        sschannel.socket().bind(new InetSocketAddress(tcpPort));
        tTCPSocketReader = new Thread(new TCPSocketReader(sschannel,
              sharedFolder, 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("Fileserver 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 (tTCPSocketReader != null && !tTCPSocketReader.isAlive())
      bailout("Listening TCP socket closed unexpected. Terminating...");
 
    shutdown();
  }
 
  /*--------------------------------------------------------------------------*/
 
  public static void main(String[] args)
  {
    try
    {
      Fileserver fserver = new Fileserver();
      fserver.run(args);
    }
    catch(Utils.Shutdown e)
    {}
  }
}