
// I2P equivalent of 'netcat'

package net.i2p.aum;

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

import net.i2p.*;
import net.i2p.client.*;
import net.i2p.client.naming.*;
import net.i2p.client.streaming.*;
import net.i2p.data.*;

import net.i2p.util.*;

/**
 * A I2P equivalent of the much-beloved 'netcat' utility.
 * This command-line utility can either connect to a remote
 * destination, or listen on a private destination for incoming
 * connections. Once a connection is established, input on stdin
 * is sent to the remote peer, and anything received from the
 * remote peer is printed to stdout
 */

public class I2PCat extends Thread
{
    public I2PSocketManager socketManager;
    public I2PServerSocket serverSocket;
    public I2PSocket sessSocket;
    
    public PrivDestination key;
    public Destination dest;
    
    public InputStream socketIn;
    public OutputStream socketOutStream;
    public OutputStreamWriter socketOut;
    
    public SockInput rxThread;
    
    protected static Log _log;

    public static String defaultHost = "127.0.0.1";
    public static int defaultPort = 7654;
    
    /**
     * a thread for reading from socket and displaying on stdout
     */
    private class SockInput extends Thread {
    
        InputStream _in;
        
        protected Log _log;
        public SockInput(InputStream i) {
        
            _in = i;
        }
        
        public void run()
        {
            // the thread portion, receives incoming bytes on
            // the socket input stream and spits them to stdout
        
            byte [] ch = new byte[1];
        
            print("Receiver thread listening...");
        
            try {
                while (true) {
        
                    //String line = DataHelper.readLine(socketIn);
                    if (_in.read(ch) != 1) {
                        print("failed to receive from socket");
                        break;
                    }
        
                    //System.out.println(line);
                    System.out.write(ch, 0, 1);
                    System.out.flush();
                }
            } catch (IOException e) {
                e.printStackTrace();
                print("Receiver thread crashed, terminating!!");
                System.exit(1);
            }
        
        }
        
        
        void print(String msg)
        {
            System.out.println("-=- I2PCat: "+msg);
        
            if (_log != null) {
                _log.debug(msg);
            }
        
        }
        
        
    }
    
    
    public I2PCat()
    {
        _log = new Log("I2PCat");
    
    }
    
    /**
     * Runs I2PCat in server mode, listening on the given destination
     * for one incoming connection. Once connection is established, 
     * copyies data between the remote peer and
     * the local terminal console.
     */
    public void runServer(String keyStr) throws IOException, DataFormatException
    {
        Properties props = new Properties();
        props.setProperty("inbound.length", "0");
        props.setProperty("outbound.length", "0");
        props.setProperty("inbound.lengthVariance", "0");
        props.setProperty("outbound.lengthVariance", "0");
    
        // generate new key if needed
        if (keyStr.equals("new")) {
    
            try {
                key = PrivDestination.newKey();
            } catch (I2PException e) {
                e.printStackTrace();
                return;
            } catch (IOException e) {
                e.printStackTrace();
                return;
            }
    
            print("Creating new server dest...");
    
            socketManager = I2PSocketManagerFactory.createManager(key.getInputStream(), props);
    
            print("Getting server socket...");
    
            serverSocket = socketManager.getServerSocket();
    
            print("Server socket created, ready to run...");
    
            dest = socketManager.getSession().getMyDestination();
            
            print("private key follows:");
            System.out.println(key.toBase64());
        
            print("dest follows:");
            System.out.println(dest.toBase64());
    
        }
    
        else {
    
            key = PrivDestination.fromBase64String(keyStr);
    
            String dest64Abbrev = key.toBase64().substring(0, 16);
    
            print("Creating server socket manager on dest "+dest64Abbrev+"...");
    
            socketManager = I2PSocketManagerFactory.createManager(key.getInputStream(), props);
    
            serverSocket = socketManager.getServerSocket();
    
            print("Server socket created, ready to run...");
        }

        print("Awaiting client connection...");
    
        I2PSocket sessSocket;
    
        try {
            sessSocket = serverSocket.accept();
        } catch (I2PException e) {
            e.printStackTrace();
            return;
        } catch (ConnectException e) {
            e.printStackTrace();
            return;
        }
                
        print("Got connection from client");
    
        chat(sessSocket);
    
    }

    public void runClient(String destStr)
        throws DataFormatException, IOException
    {
        runClient(destStr, defaultHost, defaultPort);
    }

    /**
     * runs I2PCat in client mode, connecting to a remote
     * destination then copying data between the remote peer and
     * the local terminal console
     */
    public void runClient(String destStr, String host, int port)
        throws DataFormatException, IOException
    {
        // accept 'file:' prefix
        if (destStr.startsWith("file:", 0))
        {
            String path = destStr.substring(5);
            destStr = new SimpleFile(path, "r").read();
        }
    
        else if (destStr.length() < 255) {
            // attempt hosts file lookup
            I2PAppContext ctx = new I2PAppContext();
            HostsTxtNamingService h = new HostsTxtNamingService(ctx);
            Destination dest1 = h.lookup(destStr);
            if (dest1 == null) {
                usage("Cannot resolve hostname: '"+destStr+"'");
            }
            
            // successful lookup
            runClient(dest1, host, port);
        }
    
        else {
            // otherwise, bigger strings are assumed to be base64 dests
    
            Destination dest = new Destination();
            dest.fromBase64(destStr);
            runClient(dest, host, port);
        }
    }

    public void runClient(Destination dest) {
        runClient(dest, "127.0.0.1", 7654);
    }

    /**
     * An alternative constructor which accepts an I2P Destination object
     */
    public void runClient(Destination dest, String host, int port)
    {
        this.dest = dest;
        
        String destAbbrev = dest.toBase64().substring(0, 16)+"...";
    
        print("Connecting via i2cp "+host+":"+port+" to destination "+destAbbrev+"...");
        System.out.flush();
    
        try {
            // get a socket manager
            socketManager = I2PSocketManagerFactory.createManager(host, port);

            // get a client socket
            print("socketManager="+socketManager);

            sessSocket = socketManager.connect(dest);
    
        } catch (I2PException e) {
            e.printStackTrace();
            return;
        } catch (ConnectException e) {
            e.printStackTrace();
            return;
        } catch (NoRouteToHostException e) {
            e.printStackTrace();
            return;
        } catch (InterruptedIOException e) {
            e.printStackTrace();
            return;
        }
    
        print("Successfully connected!");
        print("(Press Control-C to quit)");
    
        // Perform console interaction
        chat(sessSocket);
    
        try {
            sessSocket.close();
    
        } catch (IOException e) {
            e.printStackTrace();
            return;
        }
    }

    /**
     * Launch the background thread to copy incoming data to stdout, then
     * loop in foreground copying lines from stdin and sending them to remote peer
     */
    public void chat(I2PSocket sessSocket) {
    
        try {
            socketIn = sessSocket.getInputStream();
            socketOutStream = sessSocket.getOutputStream();
            socketOut = new OutputStreamWriter(socketOutStream);
    
            // launch receiver thread
            start();
            //launchRx();
        
            while (true) {
        
                String line = DataHelper.readLine(System.in);
                print("sent: '"+line+"'");
    
                socketOut.write(line+"\n");
                socketOut.flush();
            }
        } catch (IOException e) {
            e.printStackTrace();
            return;
        }
    
    }
    
    /**
     * executes in a thread, receiving incoming bytes on
     * the socket input stream and spitting them to stdout
     */
    public void run()
    {
    
        byte [] ch = new byte[1];
    
        print("Receiver thread listening...");
    
        try {
            while (true) {
    
                //String line = DataHelper.readLine(socketIn);
                if (socketIn.read(ch) != 1) {
                    print("failed to receive from socket");
                    break;
                }
    
                //System.out.println(line);
                System.out.write(ch, 0, 1);
                System.out.flush();
            }
        } catch (IOException e) {
            e.printStackTrace();
            print("Receiver thread crashed, terminating!!");
            System.exit(1);
        }
    
    }
    
    
    public void launchRx() {
    
        rxThread = new SockInput(socketIn);
        rxThread.start();
    
    }
    
    static void print(String msg)
    {
        System.out.println("-=- I2PCat: "+msg);
    
        if (_log != null) {
            _log.debug(msg);
        }
    
    }
    
    public static void usage(String msg)
    {
        usage(msg, 1);
    }
    
    public static void usage(String msg, int ret)
    {
        System.out.println(msg);
        usage(ret);
    }
    
    public static void usage(int ret)
    {
        System.out.print(
            "This utility is an I2P equivalent of the standard *nix 'netcat' utility\n"+
            "usage:\n"+
            "  net.i2p.aum.I2PCat [-h]\n"+
            "    - display this help\n"+
            "  net.i2p.aum.I2PCat dest [host [port]]\n"+
            "    - run in client mode, 'dest' should be one of:\n"+
            "      hostname.i2p - an I2P hostname listed in hosts.txt\n"+
            "         (only works with a hosts.txt in current directory)\n"+
            "      base64dest - a full base64 destination string\n"+
            "      file:b64filename - filename of a file containing base64 dest\n"+
            "  net.i2p.aum.I2PCat -l privkey\n"+
            "    - run in server mode, 'key' should be one of:\n"+
            "      base64privkey - a full base64 private key string\n"+
            "      file:b64filename - filename of a file containing base64 privkey\n"+
            "\n"
            );
        System.exit(ret);
    }
    
    public static void main(String [] args) throws IOException, DataFormatException
    {
        int argc = args.length;
    
        // barf if no args
        if (argc == 0) {
            usage("Missing argument");
        }
    
        // show help on request
        if (args[0].equals("-h") || args[0].equals("--help")) {
            usage(0);
        }
    
        // server or client?
        if (args[0].equals("-l")) {
            if (argc != 2) {
                usage("Bad argument count");
            }
    
            new I2PCat().runServer(args[1]);
        }
        else {
            // client mode - barf if not 1-3 args
            if (argc < 1 || argc > 3) {
                usage("Bad argument count");
            }
    
            try {
                int port = defaultPort;
                String host = defaultHost;
                if (args.length > 1) {
                    host = args[1];
                    if (args.length > 2) {
                        port = new Integer(args[2]).intValue();
                    }
                }
                new I2PCat().runClient(args[0], host, port);

            } catch (DataFormatException e) {
                e.printStackTrace();
            }
        }
    }
    
}



