package ssh;

import ssh.RSA.*;
import java.net.*;
import java.io.*;
import java.util.*;
import Tools.*;

/**
 * An adaptor to use my SSH channels with RMI from JDK 1.2, where your
 * own socket factories can supply the channels over which RMI
 * communicates.<p>
 *
 * This adaptor knows how to handle SSHOptSockets and HalfSockets, as well.
 * These classes are the implementations of SSH channel reuse and
 * channel short-circuiting (in the local case), respectively.
 */
public class SSHServerSocket
	extends java.net.ServerSocket {

	SSHContext context;
	List queue;
	AcceptThread acceptor;

	public SSHServerSocket(SSHContext context,
		int port, int backlog, InetAddress inetaddr)
		throws IOException {
		super(port, backlog, inetaddr);
		this.context = context;
		queue = new Vector();
		acceptor = null;
		registerLocal();
	}

	public SSHServerSocket(SSHContext context, int port)
		throws IOException {
		this(context, port, 5, (InetAddress) null);
	}

	static Map localSocketsMap = new HashMap();

	void registerLocal() {
		HashKey hk = new HashKey();
		hk.add(getInetAddress());
		hk.add(new Integer(getLocalPort()));
		localSocketsMap.put(hk, this);
		log.log("stored local connection: "+hk);
	}

	static Log log = new Log();
	static InetAddress everyAddress = null;
	static {
		// log.addLevel("default");
	}

	static SSHServerSocket lookForLocal(InetAddress addr, int port) {
		HashKey hk;

		SSHServerSocket ss = null;
		hk = new HashKey();
		hk.add(addr);
		hk.add(new Integer(port));
		log.log("looking for local connection: "+hk);
		ss = (SSHServerSocket) localSocketsMap.get(hk);
		if (ss!=null) {
			return ss;
		}

		// Try again with 0.0.0.0, since that's a likely registration address.
		try {
			if (everyAddress == null) {
				everyAddress = InetAddress.getByName("0.0.0.0");
			}
			hk = new HashKey();
			hk.add(everyAddress);
			hk.add(new Integer(port));
			log.log("looking for local connection: "+hk);
			return (SSHServerSocket) localSocketsMap.get(hk);
		} catch (UnknownHostException ex) {
			return null;
		}
	}

	/**
	 * If the addr/port pair is one that we are implementing in this
	 * VM, then connect to it using a purely inside-the-vm
	 * implementation of a Socket pair, and return the client half to
	 * the caller. Else return null.
	 */
	static Socket connectIfLocal(InetAddress addr, int port,
		RSAKey clientPublicKey) {
		try {
			SSHServerSocket ss = lookForLocal(addr, port);
			if (ss==null) {
				log.log("not found.");
				return null;
			}
			Socket[] socketPair = HalfSocket.createPair(ss.context.publicKey,
				clientPublicKey);
			// Now socketPair[0] is a socket whose *opposite* public key
			// is that of the server, which means we should return
			// that socket back to the client.
			// socketPair[1] is a socket whose opposite public key is
			// that of the client, so we use that here on the server end.
			ss.localConnection(socketPair[1]);
			return socketPair[0];
		} catch (IOException ex) {
			// statically thrown by createPair() (but I don't think it
			// happens in practice, since HalfSocket never calls out
			// of the JVM.)
			return null;
		}
	}

	/**
	 * Accept a connection on this socket, and run the server side of
	 * the SSH protocol on the connection to initialize it.
	 */
	public Socket accept()
		throws IOException {

		// Start acceptor thread on first accept() call
		if (acceptor==null) {
			acceptor = new AcceptThread();
			acceptor.start();
		}

		Socket s;
		synchronized(queue) {
			while (queue.size()==0) {
				try {
					queue.wait();
				} catch (InterruptedException ex) {
					// wait some more?
					// return null?
				}
			}
			s = (Socket) queue.remove(0);
		}
		if (s instanceof SSHSocket) {
			((SSHSocket) s).initServer();
		}
		return s;
	}

	/**
	 * "Listen" for local connections. The caller is a local (same-VM)
	 * client who doesn't want to deal with a network connection plus
	 * a 1500ms SSH handshake overhead. He has supplied a "socket" that
	 * acts like a network socket (notably has a working InputStream and
	 * OutputStream), but is implemented entirely inside the VM. This
	 * method takes that socket and sticks it into the accept() queue,
	 * where a thread waiting to accept() on this ServerSocket will
	 * pick it up and treat it just like an incoming network connection.
	 */
	public void localConnection(Socket s) {
		synchronized(queue) {
			queue.add(s);
			queue.notify();
		}
	}

	/**
	 * ServerSocket.implAccept() is protected, which means that
	 * nonsubclasses (such as our inner class AcceptThread) can't
	 * invoke it directly. So this method provides a route for
	 * AcceptThread.run() to get to implAccept: magicAccept() is
	 * package protected (so AcceptThread can see it), and it's in a
	 * subclass of ServerSocket, so it can see the real implAccept.
	 */
	void magicAccept(Socket s)
		throws IOException {
		implAccept(s);
	}

	/**
	 * Listen for real network connections
	 */
	public class AcceptThread extends Thread {
		public void run() {
			while (true) {
				SSHSocket s = new SSHSocket(context);
				try {
					// Have java.net.ServerSocket attach the
					// socket to the next incoming connection.
					magicAccept((Socket) s);
					// Hand the resulting socket to any waiting accept()or.
					// This strategy totally blows away the backlog parameter;
					// perhaps we should re-enforce it here by checking
					// the queue length.
					synchronized(queue) {
						queue.add(s);
						queue.notify();
					}
				} catch (IOException ex) {
					// I guess just go try again?
					// Don't want this to start spinning unnoticed, though,
					// so we'll println...
					System.err.println(ex.toString());
				}
			}
		}
	}
}
