// Java FTP Proxy Server
// by Christian Schmidt, 1998
// http://www.student.dtu.dk/~c960657/

import java.net.*;
import java.lang.*;
import java.io.*;

public class FtpProxy extends Thread {
	private Socket skControlClient, skControlServer;
	private BufferedReader brClient, brServer;
	private PrintWriter pwClient, pwServer;
	private ServerSocket ssDataClient, ssDataServer;
	private Socket skDataClient;
	private ControlConnect ccControl;
	private DataConnect dcData;
	private static int iSOTIMEOUT = 2000;
	private static int iDATASOTIMEOUT = 1000;
	private static int iDATABUFFERSIZE = 256;
	private String sLocalIP;

	private static String sServer = "Server> ";
	private static String sClient = "Client> ";
	private static String sProxy  = "Proxy > ";
	private static String sSpace  = "        ";

	public FtpProxy (Socket skControlClient) {
		this.skControlClient = skControlClient;
	}		
	
	public void run() {
		String sFromClient, sFromServer;

		ccControl = new ControlConnect();

		try {
			brClient = new BufferedReader(new InputStreamReader(skControlClient.getInputStream()));
			pwClient = new PrintWriter(skControlClient.getOutputStream(), true);
			byte b[] = skControlClient.getLocalAddress().getAddress();
			sLocalIP = (0xff & b[3]) + "," + (0xff & b[2]) + ',' + (0xff & b[1]) + ',' + (0xff & b[0]);

			sFromServer = "220 Java FTP Proxy Server (usage: USERID=user@site) ready.";
			pwClient.println(sFromServer);
			System.out.println(sProxy + sFromServer);
			
			sFromClient = brClient.readLine(); //should be "user@site"
			System.out.println(sClient + sFromClient); 
			
			int i = sFromClient.indexOf('@');
			if (i <= 5 || i + 4 >= sFromClient.length() || sFromClient.indexOf('.', i) == -1) {
				//simple validation af input
				sFromServer = "531 Incorrect usage - closing connection.";
				System.out.println(sProxy + sFromServer);
				pwClient.println(sFromServer);
				skControlClient.close();
				return;
			}

			String sUser = sFromClient.substring(0,i);
			String sServerHost = sFromClient.substring(i+1);

			System.out.println("Connecting to " + sServerHost + " on port 21");
			Socket skControlServer = new Socket(sServerHost, 21);
		
			brServer = new BufferedReader(new InputStreamReader(skControlServer.getInputStream()));
			pwServer = new PrintWriter(skControlServer.getOutputStream(), true);
		
			pwServer.println(sUser); //USER user
			System.out.println(sProxy + sUser);
			
			skControlClient.setSoTimeout(iSOTIMEOUT);
			skControlServer.setSoTimeout(iSOTIMEOUT);
			ccControl.start();
		} catch (Exception e) {
			try {
				sFromServer = "421 Internal error, closing connection.";
				System.out.println(sProxy + sFromServer + e.toString());
				pwClient.println(sFromServer);
			} catch (Exception ioe) {}
			ccControl.close();
		}
	}

	public static void main (String args[]) {
		int port = 8089;
		try {
			if (args.length == 1) 
				port = Integer.parseInt(args[0]);
			else if (args.length > 1)
				System.out.println("Usage: java FtpProxy [port]");
		} catch (NumberFormatException e) {}

						 try {
			ServerSocket ssControlClient = new ServerSocket(port);
			System.out.println("Listening on port " + port);

			while (true) {
				Socket skControlClient = ssControlClient.accept();
				System.out.println("New connection");
				new FtpProxy(skControlClient).start();
			}
						 } catch (IOException ioe) {
			System.out.println(ioe.toString());
		}
	}
	
	public class ControlConnect extends Thread {
		private boolean bClosed = false;
		private boolean bIgnoreServer = true;

		private String readLine(BufferedReader br) throws IOException {
			while (!bClosed)
				try { 
					return br.readLine();
				} catch (InterruptedIOException iioe) {}
			throw new IOException();
		}

		private synchronized void fromServer(String sFS) throws IOException {
			String sFromClient, sFromServer = sFS;

			if (!bIgnoreServer) pwClient.println(sFromServer);
			System.out.println(sServer + sFromServer);
			if (sFromServer.charAt(3) != '-') bIgnoreServer = false;
		}

		private synchronized void fromClient(String sFC) throws IOException {
			String sFromServer, sFromClient = sFC;

			if (sFromClient == null) {
				close();
				return;
			} else if (sFromClient.toUpperCase().startsWith("PASS")) {
				pwServer.println(sFromClient);
				System.out.println(sClient + "PASS *****");
			} else if (sFromClient.toUpperCase().startsWith("PASV")) {
				System.out.println(sClient + sFromClient);
	
				try {if (ssDataClient != null) ssDataClient.close();} catch (IOException ioe) {}
				try {if (skDataClient != null) skDataClient.close();} catch (IOException ioe) {}
				try {if (ssDataServer != null) ssDataServer.close();} catch (IOException ioe) {}
				if (dcData != null) dcData.close();
				skDataClient = null;

				ssDataClient = new ServerSocket(0);

				int iPort = ssDataClient.getLocalPort();
				sFromServer = "227 Entering Passive Mode (" + sLocalIP + "," + (int)(iPort/256) + "," + (iPort % 256) + ")";
				pwClient.println(sFromServer);
				System.out.println(sProxy + sFromServer);
					
				ssDataServer = new ServerSocket(0);

				iPort = ssDataServer.getLocalPort();
				sFromClient = "PORT " + sLocalIP + ',' + (int)(iPort/256) + ',' + (iPort % 256);

				pwServer.println(sFromClient);
				System.out.println(sProxy + sFromClient);

				bIgnoreServer = true;

				(dcData = new DataConnect(ssDataClient, ssDataServer)).start();
	
			} else if (sFromClient.toUpperCase().startsWith("PORT")) {
				int iClientPort, i;
				try {
					i = sFromClient.lastIndexOf(',');
					iClientPort = Integer.parseInt(sFromClient.substring(i+1));
					iClientPort += 256 * Integer.parseInt(sFromClient.substring(sFromClient.lastIndexOf(',', i-1)+1, i));
				} catch (Exception e) {throw new IOException();}

				if (ssDataClient != null) try {ssDataClient.close();} catch (IOException ioe) {}
				if (skDataClient != null) try {skDataClient.close();} catch (IOException ioe) {}
				if (ssDataServer != null) try {ssDataServer.close();} catch (IOException ioe) {}
				if (dcData != null) dcData.close();
				ssDataClient = null;

				try {
					skDataClient = new Socket(skControlClient.getInetAddress(), iClientPort);
				} catch (IOException ioe) {
					sFromServer = "500 PORT command failed - try using PASV instead.";
					pwClient.println(sFromServer);
					System.out.println(sProxy + sFromServer);
	
					System.out.println(ioe);
					return;
				}

				ssDataServer = new ServerSocket(0);

				int iPort = ssDataServer.getLocalPort();
				sFromClient = "PORT " + sLocalIP + ',' + (int)(iPort/256) + ',' + (iPort % 256);

				pwServer.println(sFromClient);
				System.out.println(sProxy + sFromClient);

				(dcData = new DataConnect(skDataClient, ssDataServer)).start();
				
			} else {
				pwServer.println(sFromClient);
				System.out.println(sClient + sFromClient);
			}
		}
	
		public void start() { 
			super.start();
			String sFromServer;
			while (!bClosed)
				try {
					if ((sFromServer = readLine(brServer)) == null) {
						break;
					}
					fromServer(sFromServer);
				} catch (IOException ioe) {
					break;
				}
			close();
		}

		public void run() {
			String sFromClient;
			while (!bClosed)
				try {
					if ((sFromClient = readLine(brClient)) == null) {
						break;
					}
					fromClient(sFromClient);
				} catch (IOException ioe) {
					break;
				}
			close();
		}
	
		public void close() {
			if (!bClosed) {
				bClosed = true;
				if (ssDataClient != null) try {ssDataClient.close();} catch (IOException ioe) {}
				if (ssDataServer != null) try {ssDataServer.close();} catch (IOException ioe) {}
				if (pwClient != null) pwClient.close();
				if (pwServer != null) pwServer.close();
				if (dcData != null) dcData.close();
			}
		}
	}

	public class DataConnect extends Thread {
		private byte bRead[] = new byte[iDATABUFFERSIZE];
		private ServerSocket ss1, ss2;
		private Socket sk1, sk2;
		boolean bClosed, bUsed, bFirst = true;

		public DataConnect (ServerSocket ss1, ServerSocket ss2) throws SocketException {
			this.ss1 = ss1;
			this.ss2 = ss2;
		}

		public DataConnect (Socket sk1, ServerSocket ss2) throws SocketException {
			this.sk1 = sk1;
			this.ss2 = ss2;
		}

		public void run() {
			BufferedInputStream bis=null;
			BufferedOutputStream bos=null;
			try {
				if (bFirst) {
					bFirst = false;
					if (ss1 != null) {
						sk1 = ss1.accept();
						ss1.close();
					}
					sk2 = ss2.accept();
					ss2.close();

					sk1.setSoTimeout(iDATASOTIMEOUT);
					sk2.setSoTimeout(iDATASOTIMEOUT);

					bis = new BufferedInputStream(sk1.getInputStream());
					bos = new BufferedOutputStream(sk2.getOutputStream());

					new Thread(this).start();
				} else {
					bis = new BufferedInputStream(sk2.getInputStream());
					bos = new BufferedOutputStream(sk1.getOutputStream());
				}

				int i;
				boolean bUsedLocal = false ;

				while (!bClosed) 
					try {
						while ((i = bis.read(bRead, 0, iDATABUFFERSIZE)) != -1) {
							bos.write(bRead, 0, i);
							bUsedLocal = true;
							bUsed = true;
						}
						break;
					} catch (InterruptedIOException iioe) {
						if (bUsed && !bUsedLocal) return; //use data connection for EITHER send or receive
					}

				bos.flush();
			} catch (IOException ioe) {
				System.out.println(ioe);
			}
			try {bis.close();} catch (Exception e) {}
			try {bos.close();} catch (Exception e) {}
			close();
		}
	
		public void close() {
			if (!bClosed) {
				bClosed = true;
				try {sk1.close();} catch (Exception e) {}
				try {sk2.close();} catch (Exception e) {}
			}
		}
	}
}
