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)
{}
}
}