TFTPServer / TFTPServer.java
TFTPServer.java
Raw
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;

public class TFTPServer 
{
	private static final String HOME_DIR = System.getProperty("user.home");


	public static final int TFTPPORT = 4970;
	public static final int BUFSIZE = 516;
	public static final int DATASIZE = 512;
	public static final String READDIR =  HOME_DIR + "/tftp/read/"; //custom address at your PC
	public static final String WRITEDIR = HOME_DIR + "/tftp/write/"; //custom address at your PC
	// OP codes
	public static final int OP_RRQ = 1;
	public static final int OP_WRQ = 2;
	public static final int OP_DAT = 3;
	public static final int OP_ACK = 4;
	public static final int OP_ERR = 5;
	// Error codes
	public static final short ERR_NDE = 0;
	public static final short ERR_FNF = 1;
	public static final short ERR_ACV = 2;
	public static final short ERR_ITO = 4;
	public static final short ERR_FAE = 6;

	public static void main(String[] args) {
		if (args.length < 0)
		{
			System.err.printf("usage: java %s\n", TFTPServer.class.getCanonicalName());
			System.exit(1);
		}
		//Starting the server
		try 
		{
			TFTPServer server= new TFTPServer();
			server.start();
		}
		catch (SocketException e) 
			{e.printStackTrace();}
		catch (IOException e)
			{e.printStackTrace();}
	}
	
	private void start() throws IOException 
	{
		byte[] buf= new byte[BUFSIZE];

		// Set timeouts for the session
		TimeOutThread.setTimeouts();
		
		// Create socket
		DatagramSocket socket= new DatagramSocket(null);
		

		// Create local bind point 
		SocketAddress localBindPoint= new InetSocketAddress(TFTPPORT);
		socket.bind(localBindPoint);


		// Create directories for read and write if non-existent
		Files.createDirectories(Paths.get(READDIR));
		Files.createDirectories(Paths.get(WRITEDIR));


		// Print out of status
		System.out.printf("Listening at port %d for new requests\n", TFTPPORT);


		// Loop to handle client requests 
		while (true) 
		{
			final InetSocketAddress clientAddress = receiveFrom(socket, buf);
			

			// If clientAddress is null, an error occurred in receiveFrom()
			if (clientAddress == null) 
				continue;

			final StringBuffer requestedFile= new StringBuffer();
			final int reqtype = ParseRQ(buf, requestedFile);

			new Thread() 
			{
				public void run() 
				{
					try 
					{
						DatagramSocket sendSocket= new DatagramSocket(0);

						// Connect to client
						sendSocket.connect(clientAddress);						

						
						// Add printout to confirm client connection and type.
						System.out.printf("%s request for %s from %s using port %d\n",
								(reqtype == OP_RRQ)?"Read":"Write",
								clientAddress.getHostName(),
								clientAddress.getAddress(),
								clientAddress.getPort());


						// Read request
						if (reqtype == OP_RRQ) 
						{
							requestedFile.insert(0, READDIR);
							HandleRQ(sendSocket, requestedFile.toString(), OP_RRQ);
						}


						// Write request
						else if(reqtype == OP_WRQ)
						{                       
							requestedFile.insert(0, WRITEDIR);
							HandleRQ(sendSocket,requestedFile.toString(),OP_WRQ);  
						}
						else
						{
							send_ERR("Malformed request", ERR_ITO, sendSocket);
						}


						// Close socket to conclude the connection
						sendSocket.close();

					}
					catch (SocketException e) 
						{e.printStackTrace();}
				}
			}.start();
		}
	}
	




	/***********************************************************************************
	 * Reads the first block of data, i.e., the request for an action (read or write). *
	 * @param socket (socket to read from)																						 *
	 * @param buf (where to store the read data)																		   *
	 * @return socketAddress (the socket address of the client)												 *
	 ***********************************************************************************/
	private InetSocketAddress receiveFrom(DatagramSocket socket, byte[] buf) 
	{
		// Create datagram packet
		DatagramPacket dp = new DatagramPacket(buf, BUFSIZE);


		// Receive packet
		try
		{socket.receive(dp);}
		catch (IOException e)
		{e.printStackTrace();}

		// Get client address and port from the packet
		InetSocketAddress socketAddress = new InetSocketAddress(dp.getAddress(), dp.getPort());

		return socketAddress;
	}



	
	/********************************************************************************
	 * Parses the request in buf to retrieve the type of request and requestedFile. *
	 * 																																							*
	 * @param buf (received request)																								*
	 * @param requestedFile (name of file to read/write)														*
	 * @return opcode (request type: RRQ or WRQ)																		*
	 ********************************************************************************/
	private int ParseRQ(byte[] buf, StringBuffer requestedFile) 
	{
		// See "TFTP Formats" in TFTP specification for the RRQ/WRQ request contents
		ByteBuffer bb = ByteBuffer.wrap(buf);
		short opcode = bb.getShort();

		// Keep adding the next byte (as char) to str buffer until null encountered.
		byte nxtByt;
		while((nxtByt = bb.get()) != (byte)0) {
			requestedFile.append((char)nxtByt);	
		}

		// TODO - Here is a good spot for handling Mode
		
		return (int)opcode;
	}




	/************************************************************
	 * Handles RRQ and WRQ requests 														*
	 * 																													*
	 * @param sendSocket (socket used to send/receive packets)	*
	 * @param requestedFile (name of file to read/write)				*
	 * @param opcode (RRQ or WRQ)																*
	 ************************************************************/
	private void HandleRQ(DatagramSocket sendSocket, String requestedFile, int opcode) 
	{
		// See "TFTP Formats" in TFTP specification for the DATA and ACK packet contents
		if(opcode == OP_RRQ)
		{
			try 
			{ 
				// Create the abstract file
				File fileToSend = new File(requestedFile);

				// Check if valid file
				if(Files.notExists(fileToSend.toPath())){
					throw new NullPointerException();
				}
				
				if (!fileToSend.canRead()){
					throw new IllegalAccessError();
				}
				

				// Proceed if a real file. Loop for all blocks until end of file.
				AtomicInteger blockNo = new AtomicInteger(1);
				while(send_DATA_receive_ACK(fileToSend, sendSocket, blockNo)) {
					blockNo.incrementAndGet();
				};
			}
			catch (IllegalAccessError ia) {
				// Access violation error needs to be sent
				System.out.println("Illegal access attempted!");
				send_ERR("Access violation", ERR_ACV, sendSocket);
			}
			catch (NullPointerException e) 
			{
				// File not found error needs to be sent
				send_ERR("File not found", ERR_FNF, sendSocket);
			}
			

			
		}
		else if (opcode == OP_WRQ) 
		{
			// Check if file already exsists - no overwriting allowed
			if (Files.exists(Paths.get(requestedFile)))
			{
				send_ERR("File already exists!", ERR_FAE, sendSocket);
			}
			else
			{
				try (FileOutputStream fileStream = new FileOutputStream(new File(requestedFile))){
					
					
					// Create vars
					AtomicInteger retrans = new AtomicInteger(0);
					AtomicInteger blockNo = new AtomicInteger(1);
					Ack firstAck = new Ack((short)OP_ACK, (short)0);


					// Send the first ACK and keep receiving until the end of file.
					sendSocket.send(new DatagramPacket(firstAck.getAckArray(), firstAck.getAckArray().length));
					while(receive_DATA_send_ACK(fileStream, sendSocket, blockNo, retrans)){
						blockNo.incrementAndGet();
					};


				} catch (IOException e) {
					send_ERR("Something went wrong. Could not receive the file.", ERR_NDE, sendSocket);
				}
				
			}
		}
		else 
		{
			System.err.println("Invalid request. Sending an error packet.");
			// See "TFTP Formats" in TFTP specification for the ERROR packet contents
			send_ERR("Cannot make out the request.", ERR_NDE, sendSocket);
			return;
		}		
	}

	


	/**************************************************
	 * Send Datagrams and handle ACKs from the client	*
	 * 																								*
	 * @param fileToSend - a java.nio.file File				*
	 * @param socket - data socket										*
	 * @return - boolean to nofify end of file				*
	 **************************************************/
	private boolean send_DATA_receive_ACK(File fileToSend, DatagramSocket socket, AtomicInteger blockNo)
	{
		ByteBuffer datagramBuff;

		// Write
		try{
			// Set timeout to prevent infinite blocking while receiving.
			socket.setSoTimeout(TimeOutThread.READRCV);


			// Declare vars for the session
			byte[] datagram;
			int writtenBytes = 0;
			long offset = (blockNo.get() - 1) * DATASIZE;
			int nxtByte;


			// Check file size and prepare block with maximum 512 bytes 
			// of data per block, according to RFC1350 hence 511.
			if (fileToSend.length() > (DATASIZE - 1))
			{
				datagram = new byte[BUFSIZE];
				datagramBuff = ByteBuffer.wrap(datagram);
			}
			else
			{
				datagram = new byte[(4 + (int)fileToSend.length())];
				datagramBuff = ByteBuffer.wrap(datagram);
			}


			// Write the headers for a packet and add 4 bits to written bytes (2xshort)
			datagramBuff.putShort((short)OP_DAT);
			datagramBuff.putShort((short)blockNo.get());
			writtenBytes += Ack.ACKSIZE;
			

			// Keep reading the file until end of stream is reached or buffer is full
			// Start is offset depending on block number.
			FileInputStream fileStream = new FileInputStream(fileToSend);
			fileStream.skip(offset);
			while((nxtByte = fileStream.read()) != -1 && writtenBytes < BUFSIZE) {
				datagramBuff.put((byte)nxtByte);
				writtenBytes ++;
			}
			fileStream.close();


			// Time-out thread to prevent getting stuck in infinite retransmission
			Thread timeOut = new Thread(new TimeOutThread());


			
			// ACK response from the client
			Ack ack = new Ack();
			

			// Start the timer and attempt to send data and receive an ACK
			timeOut.start();
			do{
				socket.send(new DatagramPacket(datagram, writtenBytes));
				try
				{socket.receive(new DatagramPacket(ack.getAckArray(), 4));}
				catch (SocketTimeoutException ste)
				{System.out.println("ACK timeout. Trying again...");}

				// Check if acknowlegement has correct opcode and block number
				ack.setAckOp();
				ack.setAckBlock();
				if(ack.getAckOp() == (short)OP_ACK && ack.getAckBlock() == blockNo.get()){break;}
			}


			// Retransmit until the package is acknowledged or the server times out.
			// while(ack.getAckOp() != (short)OP_ACK);
			while(ack.getAckOp() != (short)OP_ACK && timeOut.isAlive());


			// Malformed ack handler.
			if (ack.getAckOp() != (short)OP_ACK || ack.getAckBlock() != blockNo.get()){
				if (timeOut.isAlive()) 
				{throw new IllegalArgumentException();}
				else 
				{throw new TimeoutException();}
				
			}


			// Check if more packets need to be sent
			if(nxtByte == -1) {
				System.out.printf("%s sent successfully\n", fileToSend.getName());
				blockNo.set(0);
				return false;
			} else {
				return true;
			}

		} catch (TimeoutException te) { 
			// Handle timeout.
			String error = "Client unresponsive. Time out reached";
			System.out.println(error);
			send_ERR(error, ERR_NDE, socket);
			return false;

		} catch (Exception e) {
			// Handle errors from socket sending,reading a file stream, or malformed ACK.
			// Allow the stream to continue. Terminal output disabled due to annoyin output.

			// String error = "Malformed acknowlegement. Cannot send the block.";
			// System.out.println(error);
			blockNo.decrementAndGet();
			return true;
		}
	}
	



	// Temp addition of string to make this work
	private boolean receive_DATA_send_ACK(FileOutputStream fileToRec, DatagramSocket sock, AtomicInteger blockNo, AtomicInteger retrans)
	{
		byte[] fileBlock = new byte [BUFSIZE];
		Ack ack = new Ack((short)OP_ACK, (short)blockNo.get());
		DatagramPacket packet = new DatagramPacket(fileBlock, BUFSIZE);
		

		
		try {
			// Receive the data and wrap it in Datagram Packet
			sock.setSoTimeout(TimeOutThread.WRITERCV);
			sock.receive(packet);


			// Reset retrans for the next packet
			retrans.set(0);
			// retrans = Integer.valueOf(TimeOutThread.MAXTRANS);


			//Confirm the packet has correct OP code and continue transmission.
			if(ByteBuffer.wrap(fileBlock).getShort() != (short)OP_DAT){
				send_ERR("Incorrect Data OP code", ERR_ITO, sock);
				return true;
			}


			// Check if the block number is correct
			if(ByteBuffer.wrap(fileBlock).getShort(2) != (short)blockNo.get()){
				return true;
			}


			// Write bytes from file buffer to file stream.
			// Only writes as many bytes as were sent minus the header.
			fileToRec.write(fileBlock, Ack.ACKSIZE, (packet.getLength() - Ack.ACKSIZE));


			// Send the ACK
			sock.send(new DatagramPacket(ack.getAckArray(), ack.getAckArray().length));
			

			// Check if this was the last packet - indicated by size < 512bytes
			// Reset block counter to accept new files from the same source.
			if(packet.getLength() < BUFSIZE) {
				blockNo.set(1);
				return false;
			}
		} 
		catch (SocketTimeoutException ste){
			// Ensure block number correctness
			blockNo.decrementAndGet();
			System.out.printf("Retransmission number: %d\n",retrans.incrementAndGet());

			// Only allow MAXTRANS number of retransmissions
			// per packet to ensure no infinite loops are not created.
			// Total wait per packet is MAXTRANS * WRITERCV (30s default)
			// Send Error (Opcode 5) as a curtesy if timeout reached.
			if(retrans.get() < TimeOutThread.MAXTRANS)
			{ return true;}
			else 
			{
				send_ERR("Maximum number of retransmissions reached", ERR_NDE, sock);
				return false;
			}
		}
		catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			return false;
		}
		return true;
	}
	

	// Prepare an error datagram and send to socket
	private void send_ERR(String errMsg, short ercode, DatagramSocket sock)
	{

		// Create the array with 5 bytes constant + variable error message
		ByteBuffer errorBuffer = ByteBuffer.wrap(new byte[ (5 + errMsg.length()) ]);
		
		// Populate the array with values according to RFC1350
		// 2 bytes  2 bytes   string   1 byte
		//|  05   | errCode | errMsg |   0   |
		errorBuffer.putShort((short)OP_ERR);
		errorBuffer.putShort(ercode);
		errorBuffer.put(errMsg.getBytes());
		errorBuffer.put((byte)0);


		// Send error to sock
		try {
			sock.send(new DatagramPacket(errorBuffer.array(), errorBuffer.array().length));
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}