/* * 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 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 filelist; private Mac hmac; private final String B64 = "a-zA-Z0-9/+"; /*--------------------------------------------------------------------------*/ public Fileserver() { filelist = new HashMap(); } /*--------------------------------------------------------------------------*/ 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) {} } }