/*
 * I2PTunnel
 * (c) 2003 - 2004 mihi
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2, or (at
 * your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; see the file COPYING.  If not, write to
 * the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 *
 * In addition, as a special exception, mihi gives permission to link
 * the code of this program with the proprietary Java implementation
 * provided by Sun (or other vendors as well), and distribute linked
 * combinations including the two. You must obey the GNU General
 * Public License in all respects for all of the code used other than
 * the proprietary Java implementation. If you modify this file, you
 * may extend this exception to your version of the file, but you are
 * not obligated to do so. If you do not wish to do so, delete this
 * exception statement from your version.
 */

package net.i2p.i2ptunnel;

import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;

import net.i2p.I2PAppContext;
import net.i2p.I2PException;
import net.i2p.client.I2PClient;
import net.i2p.client.I2PClientFactory;
import net.i2p.client.I2PSession;
import net.i2p.client.naming.NamingService;
import net.i2p.data.Base64;
import net.i2p.data.DataFormatException;
import net.i2p.data.DataHelper;
import net.i2p.data.Destination;
import net.i2p.i2ptunnel.socks.I2PSOCKSTunnel;
import net.i2p.util.EventDispatcher;
import net.i2p.util.EventDispatcherImpl;
import net.i2p.util.Log;

public class I2PTunnel implements Logging, EventDispatcher {
    private Log _log;
    private EventDispatcherImpl _event;
    private I2PAppContext _context;
    private static long __tunnelId = 0;
    private long _tunnelId;
    private Properties _clientOptions;
    private List _sessions;

    public static final int PACKET_DELAY = 100;

    public boolean ownDest = false;

    public String port = System.getProperty(I2PClient.PROP_TCP_PORT, "7654");
    public String host = System.getProperty(I2PClient.PROP_TCP_HOST, "127.0.0.1");
    public String listenHost = host;

    public long readTimeout = -1;

    private static final String nocli_args[] = { "-nocli", "-die"};

    private List tasks = new ArrayList();
    private int next_task_id = 1;

    private Set listeners = new HashSet();

    public static void main(String[] args) throws IOException {
        new I2PTunnel(args);
    }

    public I2PTunnel() {
        this(nocli_args);
    }

    public I2PTunnel(String[] args) {
        this(args, null);
    }

    public I2PTunnel(String[] args, ConnectionEventListener lsnr) {
        _context = I2PAppContext.getGlobalContext(); // new I2PAppContext();
        _tunnelId = ++__tunnelId;
        _log = _context.logManager().getLog(I2PTunnel.class);
        _event = new EventDispatcherImpl();
        Properties p = new Properties();
        p.putAll(System.getProperties());
        _clientOptions = p;
        _sessions = new ArrayList(1);
        
        addConnectionEventListener(lsnr);
        boolean gui = true;
        boolean checkRunByE = true;
        boolean cli = true;
        boolean dontDie = true;
        for (int i = 0; i < args.length; i++) {
            if (args[i].equals("-die")) {
                dontDie = false;
                gui = false;
                cli = false;
                checkRunByE = false;
            } else if (args[i].equals("-nogui")) {
                gui = false;
                _log.warn(getPrefix() + "The `-nogui' option of I2PTunnel is deprecated.\n"
                          + "Use `-cli', `-nocli' (aka `-wait') or `-die' instead.");
            } else if (args[i].equals("-cli")) {
                gui = false;
                cli = true;
                checkRunByE = false;
            } else if (args[i].equals("-nocli") || args[i].equals("-wait")) {
                gui = false;
                cli = false;
                checkRunByE = false;
            } else if (args[i].equals("-e")) {
                runCommand(args[i + 1], this);
                i++;
                if (checkRunByE) {
                    checkRunByE = false;
                    cli = false;
                }
            } else if (new File(args[i]).exists()) {
                runCommand("run " + args[i], this);
            } else {
                System.out.println("Unknown parameter " + args[i]);
            }
        }
        if (gui) {
            new I2PTunnelGUI(this);
        } else if (cli) {
            try {
                System.out.println("Enter 'help' for help.");
                BufferedReader r = new BufferedReader(new InputStreamReader(System.in));
                while (true) {
                    System.out.print("I2PTunnel>");
                    String cmd = r.readLine();
                    if (cmd == null) break;
                    runCommand(cmd, this);
                }
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }

        while (dontDie) {
            synchronized (this) {
                try {
                    wait();
                } catch (InterruptedException ie) {
                }
            }
        }
    }

    List getSessions() { 
        synchronized (_sessions) {
            return new ArrayList(_sessions); 
        }
    }
    void addSession(I2PSession session) { 
        if (session == null) return;
        synchronized (_sessions) {
            if (!_sessions.contains(session))
                _sessions.add(session);
        }
    }
    void removeSession(I2PSession session) { 
        if (session == null) return;
        synchronized (_sessions) {
            _sessions.remove(session);
        }
    }
    
    public Properties getClientOptions() { return _clientOptions; }
    
    private void addtask(I2PTunnelTask tsk) {
        tsk.setTunnel(this);
        if (tsk.isOpen()) {
            tsk.setId(next_task_id);
            next_task_id++;
            synchronized (tasks) {
                tasks.add(tsk);
            }
        }
    }

    /** java 1.3 vs 1.4 :)
     */
    private static String[] split(String src, String delim) {
        StringTokenizer tok = new StringTokenizer(src, delim);
        String vals[] = new String[tok.countTokens()];
        for (int i = 0; i < vals.length; i++)
            vals[i] = tok.nextToken();
        return vals;
    }

    public void runCommand(String cmd, Logging l) {
        if (cmd.indexOf(" ") == -1) cmd += " ";
        int iii = cmd.indexOf(" ");
        String cmdname = cmd.substring(0, iii).toLowerCase();
        String allargs = cmd.substring(iii + 1);
        String[] args = split(allargs, " "); // .split(" "); // java 1.4

        if ("help".equals(cmdname)) {
            runHelp(l);
        } else if ("clientoptions".equals(cmdname)) {
            runClientOptions(args, l);
        } else if ("server".equals(cmdname)) {
            runServer(args, l);
        } else if ("httpserver".equals(cmdname)) {
            runHttpServer(args, l);
        } else if ("textserver".equals(cmdname)) {
            runTextServer(args, l);
        } else if ("client".equals(cmdname)) {
            runClient(args, l);
        } else if ("httpclient".equals(cmdname)) {
            runHttpClient(args, l);
        } else if ("ircclient".equals(cmdname)) {
            runIrcClient(args, l);
        } else if ("sockstunnel".equals(cmdname)) {
            runSOCKSTunnel(args, l);
        } else if ("config".equals(cmdname)) {
            runConfig(args, l);
        } else if ("listen_on".equals(cmdname)) {
            runListenOn(args, l);
        } else if ("read_timeout".equals(cmdname)) {
            runReadTimeout(args, l);
        } else if ("genkeys".equals(cmdname)) {
            runGenKeys(args, l);
        } else if ("gentextkeys".equals(cmdname)) {
            runGenTextKeys(l);
        } else if (cmdname.equals("quit")) {
            runQuit(l);
        } else if (cmdname.equals("list")) {
            runList(l);
        } else if (cmdname.equals("close")) {
            runClose(args, l);
        } else if (cmdname.equals("run")) {
            runRun(args, l);
        } else if (cmdname.equals("lookup")) {
            runLookup(args, l);
        } else if (cmdname.equals("ping")) {
            runPing(allargs, l);
        } else if (cmdname.equals("owndest")) {
            runOwnDest(args, l);
        } else {
            l.log("Unknown command [" + cmdname + "]");
        }
    }

    /**
     * Display help information through the given logger.
     *
     * Does not fire any events to the logger
     *
     * @param l logger to receive events and output
     */
    public void runHelp(Logging l) {
        l.log("Command list:");
        l.log("config <i2phost> <i2pport>");
        l.log("listen_on <ip>");
        l.log("read_timeout <msecs>");
        l.log("owndest yes|no");
        l.log("ping <args>");
        l.log("server <host> <port> <privkeyfile>");
        l.log("httpserver <host> <port> <spoofedhost> <privkeyfile>");
        l.log("textserver <host> <port> <privkey>");
        l.log("genkeys <privkeyfile> [<pubkeyfile>]");
        l.log("gentextkeys");
        l.log("client <port> <pubkey>[,<pubkey,...]|file:<pubkeyfile> [<sharedClient>]");
        l.log("ircclient <port> <pubkey>[,<pubkey,...]|file:<pubkeyfile> [<sharedClient>]");
        l.log("httpclient <port> [<sharedClient>] [<proxy>]");
        l.log("lookup <name>");
        l.log("quit");
        l.log("close [forced] <jobnumber>|all");
        l.log("list");
        l.log("run <commandfile>");
    }
    
    /**
     * Configure the extra I2CP options to use in any subsequent I2CP sessions.
     * Usage: "clientoptions[ key=value]*" .  
     *
     * Sets the event "clientoptions_onResult" = "ok" after completion.
     *
     * @param args each args[i] is a key=value pair to add to the options
     * @param l logger to receive events and output
     */
    public void runClientOptions(String args[], Logging l) {
        _clientOptions.clear();
        if (args != null) {
            for (int i = 0; i < args.length; i++) {
                int index = args[i].indexOf('=');
                if (index <= 0) continue;
                String key = args[i].substring(0, index);
                String val = args[i].substring(index+1);
                _clientOptions.setProperty(key, val);
            }
        }
        notifyEvent("clientoptions_onResult", "ok");
    }

    /**
     * Run the server pointing at the host and port specified using the private i2p
     * destination loaded from the specified file. <p />
     *
     * Sets the event "serverTaskId" = Integer(taskId) after the tunnel has been started (or -1 on error)
     * Also sets the event "openServerResult" = "ok" or "error" (displaying "Ready!" on the logger after
     * 'ok').  So, success = serverTaskId != -1 and openServerResult = ok.
     *
     * @param args {hostname, portNumber, privKeyFilename}
     * @param l logger to receive events and output
     */
    public void runServer(String args[], Logging l) {
        if (args.length == 3) {
            InetAddress serverHost = null;
            int portNum = -1;
            File privKeyFile = null;
            try {
                serverHost = InetAddress.getByName(args[0]);
            } catch (UnknownHostException uhe) {
                l.log("unknown host");
                _log.error(getPrefix() + "Error resolving " + args[0], uhe);
                notifyEvent("serverTaskId", new Integer(-1));
                return;
            }

            try {
                portNum = Integer.parseInt(args[1]);
            } catch (NumberFormatException nfe) {
                l.log("invalid port");
                _log.error(getPrefix() + "Port specified is not valid: " + args[1], nfe);
                notifyEvent("serverTaskId", new Integer(-1));
                return;
            }

            privKeyFile = new File(args[2]);
            if (!privKeyFile.canRead()) {
                l.log("private key file does not exist");
                _log.error(getPrefix() + "Private key file does not exist or is not readable: " + args[2]);
                notifyEvent("serverTaskId", new Integer(-1));
                return;
            }
            I2PTunnelServer serv = new I2PTunnelServer(serverHost, portNum, privKeyFile, args[2], l, (EventDispatcher) this, this);
            serv.setReadTimeout(readTimeout);
            serv.startRunning();
            addtask(serv);
            notifyEvent("serverTaskId", new Integer(serv.getId()));
            return;
        } else {
            l.log("server <host> <port> <privkeyfile>");
            l.log("  creates a server that sends all incoming data\n" + "  of its destination to host:port.");
            notifyEvent("serverTaskId", new Integer(-1));
        }
    }

    /**
     * Run the HTTP server pointing at the host and port specified using the private i2p
     * destination loaded from the specified file, replacing the HTTP headers
     * so that the Host: specified is the one spoofed. <p />
     *
     * Sets the event "serverTaskId" = Integer(taskId) after the tunnel has been started (or -1 on error)
     * Also sets the event "openServerResult" = "ok" or "error" (displaying "Ready!" on the logger after
     * 'ok').  So, success = serverTaskId != -1 and openServerResult = ok.
     *
     * @param args {hostname, portNumber, spoofedHost, privKeyFilename}
     * @param l logger to receive events and output
     */
    public void runHttpServer(String args[], Logging l) {
        if (args.length == 4) {
            InetAddress serverHost = null;
            int portNum = -1;
            File privKeyFile = null;
            try {
                serverHost = InetAddress.getByName(args[0]);
            } catch (UnknownHostException uhe) {
                l.log("unknown host");
                _log.error(getPrefix() + "Error resolving " + args[0], uhe);
                notifyEvent("serverTaskId", new Integer(-1));
                return;
            }

            try {
                portNum = Integer.parseInt(args[1]);
            } catch (NumberFormatException nfe) {
                l.log("invalid port");
                _log.error(getPrefix() + "Port specified is not valid: " + args[1], nfe);
                notifyEvent("serverTaskId", new Integer(-1));
                return;
            }

            String spoofedHost = args[2];
            
            privKeyFile = new File(args[3]);
            if (!privKeyFile.canRead()) {
                l.log("private key file does not exist");
                _log.error(getPrefix() + "Private key file does not exist or is not readable: " + args[3]);
                notifyEvent("serverTaskId", new Integer(-1));
                return;
            }
            I2PTunnelHTTPServer serv = new I2PTunnelHTTPServer(serverHost, portNum, privKeyFile, args[3], spoofedHost, l, (EventDispatcher) this, this);
            serv.setReadTimeout(readTimeout);
            serv.startRunning();
            addtask(serv);
            notifyEvent("serverTaskId", new Integer(serv.getId()));
            return;
        } else {
            l.log("httpserver <host> <port> <spoofedhost> <privkeyfile>");
            l.log("  creates an HTTP server that sends all incoming data\n" 
                  + "  of its destination to host:port., filtering the HTTP\n" 
                  + "  headers so it looks like the request is to the spoofed host.");
            notifyEvent("serverTaskId", new Integer(-1));
        }
    }

    /**
     * Run the server pointing at the host and port specified using the private i2p
     * destination loaded from the given base64 stream. <p />
     *
     * Sets the event "serverTaskId" = Integer(taskId) after the tunnel has been started (or -1 on error)
     * Also sets the event "openServerResult" = "ok" or "error" (displaying "Ready!" on the logger after
     * 'ok').  So, success = serverTaskId != -1 and openServerResult = ok.
     *
     * @param args {hostname, portNumber, privKeyBase64}
     * @param l logger to receive events and output
     */
    public void runTextServer(String args[], Logging l) {
        if (args.length == 3) {
            InetAddress serverHost = null;
            int portNum = -1;
            try {
                serverHost = InetAddress.getByName(args[0]);
            } catch (UnknownHostException uhe) {
                l.log("unknown host");
                _log.error(getPrefix() + "Error resolving " + args[0], uhe);
                notifyEvent("serverTaskId", new Integer(-1));
                return;
            }

            try {
                portNum = Integer.parseInt(args[1]);
            } catch (NumberFormatException nfe) {
                l.log("invalid port");
                _log.error(getPrefix() + "Port specified is not valid: " + args[1], nfe);
                notifyEvent("serverTaskId", new Integer(-1));
                return;
            }

            I2PTunnelServer serv = new I2PTunnelServer(serverHost, portNum, args[2], l, (EventDispatcher) this, this);
            serv.setReadTimeout(readTimeout);
            serv.startRunning();
            addtask(serv);
            notifyEvent("serverTaskId", new Integer(serv.getId()));
        } else {
            l.log("textserver <host> <port> <privkey>");
            l.log("  creates a server that sends all incoming data\n" + "  of its destination to host:port.");
            notifyEvent("textserverTaskId", new Integer(-1));
        }
    }

    /**
     * Run the client on the given port number pointing at the specified destination
     * (either the base64 of the destination or file:fileNameContainingDestination).
     *
     * Sets the event "clientTaskId" = Integer(taskId) after the tunnel has been started (or -1 on error)
     * Also sets the event "openClientResult" = "error" or "ok" (before setting the value to "ok" it also
     * adds "Ready! Port #" to the logger as well).  In addition, it will also set "clientLocalPort" =
     * Integer port number if the client is listening
     * sharedClient parameter is a String "true" or "false"
     *
     * @param args {portNumber, destinationBase64 or "file:filename"[, sharedClient]}
     * @param l logger to receive events and output
     */
    public void runClient(String args[], Logging l) {
        boolean isShared = true;
        if (args.length == 3)
            isShared = Boolean.valueOf(args[2].trim()).booleanValue();
        if ( (args.length == 2) || (args.length == 3) ) {
            int portNum = -1;
            try {
                portNum = Integer.parseInt(args[0]);
            } catch (NumberFormatException nfe) {
                l.log("invalid port");
                _log.error(getPrefix() + "Port specified is not valid: " + args[0], nfe);
                notifyEvent("clientTaskId", new Integer(-1));
                return;
            }
            I2PTunnelTask task;
            ownDest = !isShared;
            try {
                task = new I2PTunnelClient(portNum, args[1], l, ownDest, (EventDispatcher) this, this);
                addtask(task);
                notifyEvent("clientTaskId", new Integer(task.getId()));
            } catch (IllegalArgumentException iae) {
                _log.error(getPrefix() + "Invalid I2PTunnel config to create a client [" + host + ":"+ port + "]", iae);
                l.log("Invalid I2PTunnel configuration [" + host + ":" + port + "]");
                notifyEvent("clientTaskId", new Integer(-1));
            }
        } else {
            l.log("client <port> <pubkey>[,<pubkey>]|file:<pubkeyfile>[ <sharedClient>]");
            l.log("  creates a client that forwards port to the pubkey.\n"
                  + "  use 0 as port to get a free port assigned.  If you specify\n"
                  + "  a comma delimited list of pubkeys, it will rotate among them\n"
                  + "  randomlyl. sharedClient indicates if this client shares \n"
                  + "   with other clients (true of false)");
            notifyEvent("clientTaskId", new Integer(-1));
        }
    }

    /**
     * Run an HTTP client on the given port number 
     *
     * Sets the event "httpclientTaskId" = Integer(taskId) after the tunnel has been started (or -1 on error).
     * Also sets "httpclientStatus" = "ok" or "error" after the client tunnel has started.
     * parameter sharedClient is a String, either "true" or "false"
     *
     * @param args {portNumber[, sharedClient][, proxy to be used for the WWW]}
     * @param l logger to receive events and output
     */
    public void runHttpClient(String args[], Logging l) {
        if (args.length >= 1 && args.length <= 3) {
            int port = -1;
            try {
                port = Integer.parseInt(args[0]);
            } catch (NumberFormatException nfe) {
                l.log("invalid port");
                _log.error(getPrefix() + "Port specified is not valid: " + args[0], nfe);
                notifyEvent("httpclientTaskId", new Integer(-1));
                return;
            }
            
            String proxy = "squid.i2p";
            boolean isShared = true;
            if (args.length > 1) {
                if ("true".equalsIgnoreCase(args[1].trim())) {
                    isShared = true;
                    if (args.length == 3)
                        proxy = args[2];
                } else if ("false".equalsIgnoreCase(args[1].trim())) {
                    _log.warn("args[1] == [" + args[1] + "] and rejected explicitly");
                    isShared = false;
                    if (args.length == 3)
                        proxy = args[2];
                } else if (args.length == 3) {
                    isShared = false; // not "true"
                    proxy = args[2];
                    _log.warn("args[1] == [" + args[1] + "] but rejected");
                } else {
                    // isShared not specified, default to true
                    isShared = true;
                    proxy = args[1];
                }
            }

            I2PTunnelTask task;
            ownDest = !isShared;
            try {
                task = new I2PTunnelHTTPClient(port, l, ownDest, proxy, (EventDispatcher) this, this);
                addtask(task);
                notifyEvent("httpclientTaskId", new Integer(task.getId()));
            } catch (IllegalArgumentException iae) {
                _log.error(getPrefix() + "Invalid I2PTunnel config to create an httpclient [" + host + ":"+ port + "]", iae);
                l.log("Invalid I2PTunnel configuration [" + host + ":" + port + "]");
                notifyEvent("httpclientTaskId", new Integer(-1));
            }
        } else {
            l.log("httpclient <port> [<sharedClient>] [<proxy>]");
            l.log("  creates a client that distributes HTTP requests.");
            l.log("  <sharedClient> (optional) indicates if this client shares tunnels with other clients (true of false)");
            l.log("  <proxy> (optional) indicates a proxy server to be used");
            l.log("  when trying to access an address out of the .i2p domain");
            l.log("  (the default proxy is squid.i2p).");
            notifyEvent("httpclientTaskId", new Integer(-1));
        }
    }

    /**
     * Run an IRC client on the given port number 
     *
     * Sets the event "ircclientTaskId" = Integer(taskId) after the tunnel has been started (or -1 on error).
     * Also sets "ircclientStatus" = "ok" or "error" after the client tunnel has started.
     * parameter sharedClient is a String, either "true" or "false"
     *
     * @param args {portNumber,destinationBase64 or "file:filename" [, sharedClient]}
     * @param l logger to receive events and output
     */
    public void runIrcClient(String args[], Logging l) {
        if (args.length >= 2 && args.length <= 3) {
            int port = -1;
            try {
                port = Integer.parseInt(args[0]);
            } catch (NumberFormatException nfe) {
                l.log("invalid port");
                _log.error(getPrefix() + "Port specified is not valid: " + args[0], nfe);
                notifyEvent("ircclientTaskId", new Integer(-1));
                return;
            }
            
            boolean isShared = true;
            if (args.length > 2) {
                if ("true".equalsIgnoreCase(args[2].trim())) {
                    isShared = true;
                } else if ("false".equalsIgnoreCase(args[2].trim())) {
                    _log.warn("args[2] == [" + args[2] + "] and rejected explicitly");
                    isShared = false;
                } else {
                    // isShared not specified, default to true
                    isShared = true;
                }
            }

            I2PTunnelTask task;
            ownDest = !isShared;
            try {
                task = new I2PTunnelIRCClient(port, args[1],l, ownDest, (EventDispatcher) this, this);
                addtask(task);
                notifyEvent("ircclientTaskId", new Integer(task.getId()));
            } catch (IllegalArgumentException iae) {
                _log.error(getPrefix() + "Invalid I2PTunnel config to create an ircclient [" + host + ":"+ port + "]", iae);
                l.log("Invalid I2PTunnel configuration [" + host + ":" + port + "]");
                notifyEvent("ircclientTaskId", new Integer(-1));
            }
        } else {
            l.log("ircclient <port> [<sharedClient>]");
            l.log("  creates a client that filter IRC protocol.");
            l.log("  <sharedClient> (optional) indicates if this client shares tunnels with other clients (true of false)");
            notifyEvent("ircclientTaskId", new Integer(-1));
        }
    }
    
    /**
     * Run an SOCKS tunnel on the given port number 
     *
     * Sets the event "sockstunnelTaskId" = Integer(taskId) after the
     * tunnel has been started (or -1 on error).  Also sets
     * "openSOCKSTunnelResult" = "ok" or "error" after the client tunnel has
     * started.
     *
     * @param args {portNumber}
     * @param l logger to receive events and output
     */
    public void runSOCKSTunnel(String args[], Logging l) {
        if (args.length >= 1 && args.length <= 2) {
            int port = -1;
            try {
                port = Integer.parseInt(args[0]);
            } catch (NumberFormatException nfe) {
                l.log("invalid port");
                _log.error(getPrefix() + "Port specified is not valid: " + args[0], nfe);
                notifyEvent("sockstunnelTaskId", new Integer(-1));
                return;
            }

            I2PTunnelTask task;
            task = new I2PSOCKSTunnel(port, l, ownDest, (EventDispatcher) this, this);
            addtask(task);
            notifyEvent("sockstunnelTaskId", new Integer(task.getId()));
        } else {
            l.log("sockstunnel <port>");
            l.log("  creates a tunnel that distributes SOCKS requests.");
            notifyEvent("sockstunnelTaskId", new Integer(-1));
        }
    }

    /**
     * Specify the i2cp host and port 
     *
     * Sets the event "configResult" = "ok" or "error" after the configuration has been specified
     *
     * @param args {hostname, portNumber}
     * @param l logger to receive events and output
     */
    public void runConfig(String args[], Logging l) {
        if (args.length == 2) {
            host = args[0];
            listenHost = host;
            port = args[1];
            notifyEvent("configResult", "ok");
        } else {
            l.log("config <i2phost> <i2pport>");
            l.log("  sets the connection to the i2p router.");
            notifyEvent("configResult", "error");
        }
    }

    /**
     * Specify whether to use its own destination for each outgoing tunnel
     *
     * Sets the event "owndestResult" = "ok" or "error" after the configuration has been specified
     *
     * @param args {yes or no}
     * @param l logger to receive events and output
     */
    public void runOwnDest(String args[], Logging l) {
        if (args.length == 1 && (args[0].equalsIgnoreCase("yes") || args[0].equalsIgnoreCase("no"))) {
            ownDest = args[0].equalsIgnoreCase("yes");
            notifyEvent("owndestResult", "ok");
        } else {
            l.log("owndest yes|no");
            l.log("  Specifies whether to use its own destination \n" + "  for each outgoing tunnel");
            notifyEvent("owndestResult", "error");
        }
    }

    /**
     * Specify the hostname / IP address of the interface that the tunnels should bind to
     *
     * Sets the event "listen_onResult" = "ok" or "error" after the interface has been specified
     *
     * @param args {hostname}
     * @param l logger to receive events and output
     */
    public void runListenOn(String args[], Logging l) {
        if (args.length == 1) {
            listenHost = args[0];
            notifyEvent("listen_onResult", "ok");
        } else {
            l.log("listen_on <ip>");
            l.log("  sets the interface to listen for the I2PClient.");
            notifyEvent("listen_onResult", "error");
        }
    }

    /**
     * Specify the read timeout going to be used for newly-created I2PSockets
     *
     * Sets the event "read_timeoutResult" = "ok" or "error" after the interface has been specified
     *
     * @param args {hostname}
     * @param l logger to receive events and output
     */
    public void runReadTimeout(String args[], Logging l) {
        if (args.length == 1) {
            try {
                readTimeout = Long.parseLong(args[0]);
            } catch (NumberFormatException e) {
                notifyEvent("read_timeoutResult", "error");
            }
            notifyEvent("read_timeoutResult", "ok");
        } else {
            l.log("read_timeout <msecs>");
            l.log("  sets the read timeout (in milliseconds) for I2P connections\n"
                  +"  Negative values will make the connections wait forever");
            notifyEvent("read_timeoutResult", "error");
        }
    }

    /**
     * Generate a new keypair
     *
     * Sets the event "genkeysResult" = "ok" or "error" after the generation is complete
     *
     * @param args {privateKeyFilename, publicKeyFilename} or {privateKeyFilename}
     * @param l logger to receive events and output
     */
    public void runGenKeys(String args[], Logging l) {
        OutputStream pubdest = null;
        if (args.length == 2) {
            try {
                pubdest = new FileOutputStream(args[1]);
            } catch (IOException ioe) {
                l.log("Error opening output stream");
                _log.error(getPrefix() + "Error generating keys to out", ioe);
                notifyEvent("genkeysResult", "error");
                return;
            }
        } else if (args.length != 1) {
            l.log("genkeys <privkeyfile> [<pubkeyfile>]");
            l.log("   creates a new keypair and prints the public key.\n"
                  + "   if pubkeyfile is given, saves the public key there." + "\n"
                  + "   if the privkeyfile already exists, just print/save" + "the pubkey.");
            notifyEvent("genkeysResult", "error");
        }
        try {
            File privKeyFile = new File(args[0]);
            if (privKeyFile.exists()) {
                l.log("File already exists.");
                showKey(new FileInputStream(privKeyFile), pubdest, l);
            } else {
                makeKey(new FileOutputStream(privKeyFile), pubdest, l);
            }
            notifyEvent("genkeysResult", "ok");
        } catch (IOException ioe) {
            l.log("Error generating keys - " + ioe.getMessage());
            notifyEvent("genkeysResult", "error");
            _log.error(getPrefix() + "Error generating keys", ioe);
        }
    }

    /**
     * Generate a new keypair
     *
     * Sets the event "privateKey" = base64 of the privateKey stream and
     * sets the event "publicDestination" = base64 of the destination
     *
     * @param l logger to receive events and output
     */
    public void runGenTextKeys(Logging l) {
        ByteArrayOutputStream privkey = new ByteArrayOutputStream(512);
        ByteArrayOutputStream pubkey = new ByteArrayOutputStream(512);
        makeKey(privkey, pubkey, l);
        l.log("Private key: " + Base64.encode(privkey.toByteArray()));
        notifyEvent("privateKey", Base64.encode(privkey.toByteArray()));
        notifyEvent("publicDestination", Base64.encode(pubkey.toByteArray()));
    }

    /**
     * Exit the JVM if there are no more tasks left running.  If there are tunnels
     * running, it returns.
     *
     * Sets the event "quitResult" = "error" if there are tasks running (but if there
     * aren't, well, there's no point in setting the quitResult to "ok", now is there?)
     *
     * @param l logger to receive events and output
     */
    public void runQuit(Logging l) {
        purgetasks(l);
        synchronized (tasks) {
            if (tasks.isEmpty()) {
                System.exit(0);
            }
        }
        l.log("There are running tasks. Try 'list'.");
        notifyEvent("quitResult", "error");
    }

    /**
     * Retrieve a list of currently running tasks
     *
     * Sets the event "listDone" = "done" after dumping the tasks to
     * the logger
     *
     * @param l logger to receive events and output
     */
    public void runList(Logging l) {
        purgetasks(l);
        synchronized (tasks) {
            for (int i = 0; i < tasks.size(); i++) {
                I2PTunnelTask t = (I2PTunnelTask) tasks.get(i);
                l.log("[" + t.getId() + "] " + t.toString());
            }
        }
        notifyEvent("listDone", "done");
    }

    /**
     * Close the given task (or all tasks), optionally forcing them to die a hard
     * death
     *
     * Sets the event "closeResult" = "ok" after the closing is complete
     *
     * @param args {jobNumber}, {"forced", jobNumber}, {"forced", "all"}
     * @param l logger to receive events and output
     */
    public void runClose(String args[], Logging l) {
        if (args.length == 0 || args.length > 2) {
            l.log("close [forced] <jobnumber>|all");
            l.log("   stop running tasks. either only one or all.\n"
                  + "   use 'forced' to also stop tasks with active connections.\n"
                  + "   use the 'list' command to show the job numbers");
            notifyEvent("closeResult", "error");
        } else {
            int argindex = 0; // parse optional 'forced' keyword
            boolean forced = false;
            if (args[argindex].equalsIgnoreCase("forced")) {
                forced = true;
                argindex++;
            }
            if (args[argindex].equalsIgnoreCase("all")) {
                List curTasks = null;
                synchronized (tasks) {
                    curTasks = new LinkedList(tasks);
                }

                boolean error = false;
                for (int i = 0; i < curTasks.size(); i++) {
                    I2PTunnelTask t = (I2PTunnelTask) curTasks.get(i);
                    if (!closetask(t, forced, l)) {
                        notifyEvent("closeResult", "error");
                        error = true;
                    } else if (!error) { // If there's an error, don't hide it
                        notifyEvent("closeResult", "ok");
                    }
                }
            } else {
                try {
                    if (!closetask(Integer.parseInt(args[argindex]), forced, l)) {
                        notifyEvent("closeResult", "error");
                    } else {
                        notifyEvent("closeResult", "ok");
                    }
                } catch (NumberFormatException ex) {
                    l.log("Incorrect job number: " + args[argindex]);
                    notifyEvent("closeResult", "error");
                }
            }
        }
    }

    /**
     * Run all of the commands in the given file (one command per line)
     *
     * Sets the event "runResult" = "ok" or "error" after the closing is complete
     *
     * @param args {filename}
     * @param l logger to receive events and output
     */
    public void runRun(String args[], Logging l) {
        if (args.length == 1) {
            try {
                BufferedReader br = new BufferedReader(new FileReader(args[0]));
                String line;
                while ((line = br.readLine()) != null) {
                    runCommand(line, l);
                }
                br.close();
                notifyEvent("runResult", "ok");
            } catch (IOException ioe) {
                l.log("IO error running the file");
                _log.error(getPrefix() + "Error running the file", ioe);
                notifyEvent("runResult", "error");
            }
        } else {
            l.log("run <commandfile>");
            l.log("   loads commandfile and runs each line in it. \n"
                  + "   You can also give the filename on the commandline.");
            notifyEvent("runResult", "error");
        }
    }

    /**
     * Perform a lookup of the name specified
     *
     * Sets the event "lookupResult" = base64 of the destination, or an error message 
     *
     * @param args {name}
     * @param l logger to receive events and output
     */
    public void runLookup(String args[], Logging l) {
        if (args.length != 1) {
            l.log("lookup <name>");
            l.log("   try to resolve the name into a destination key");
            notifyEvent("lookupResult", "invalidUsage");
        } else {
            String target = args[0];
            try {
                Destination dest = destFromName(args[0]);
                if (dest == null) {
                    l.log("Unknown host");
                    notifyEvent("lookupResult", "unkown host");
                } else {
                    l.log(dest.toBase64());
                    notifyEvent("lookupResult", dest.toBase64());
                }
            } catch (DataFormatException dfe) {
                l.log("Unknown or invalid host");
                notifyEvent("lookupResult", "invalid host");
            }
        }
    }

    /**
     * Start up a ping task with the specified args (currently supporting -ns, -h, -l)
     *
     * Sets the event "pingTaskId" = Integer of the taskId, or -1
     *
     * @param allargs arguments to pass to the I2Ping task
     * @param l logger to receive events and output
     */
    public void runPing(String allargs, Logging l) {
        if (allargs.length() != 0) {
            I2PTunnelTask task;
            // pings always use the main destination
            task = new I2Ping(allargs, l, false, (EventDispatcher) this, this);
            addtask(task);
            notifyEvent("pingTaskId", new Integer(task.getId()));
        } else {
            l.log("ping <opts> <dest>");
            l.log("ping <opts> -h");
            l.log("ping <opts> -l <destlistfile>");
            l.log("   Tests communication with peers.\n" + "   opts can be -ns (nosync) or not.");
            notifyEvent("pingTaskId", new Integer(-1));
        }
    }

    /**
     * Helper method to actually close the given task number (optionally forcing 
     * closure)
     *
     */
    private boolean closetask(int num, boolean forced, Logging l) {
        boolean closed = false;

        _log.debug(getPrefix() + "closetask(): looking for task " + num);
        synchronized (tasks) {
            for (Iterator it = tasks.iterator(); it.hasNext();) {
                I2PTunnelTask t = (I2PTunnelTask) it.next();
                int id = t.getId();
                _log.debug(getPrefix() + "closetask(): parsing task " + id + " (" + t.toString() + ")");
                if (id == num) {
                    closed = closetask(t, forced, l);
                    break;
                } else if (id > num) {
                    break;
                }
            }
        }
        return closed;
    }

    /**
     * Helper method to actually close the given task number
     * (optionally forcing closure)
     *
     */
    private boolean closetask(I2PTunnelTask t, boolean forced, Logging l) {
        l.log("Closing task " + t.getId() + (forced ? " forced..." : "..."));
        if (t.close(forced)) {
            l.log("Task " + t.getId() + " closed.");
            return true;
        }
        return false;
    }

    /**
     * Helper task to remove closed / completed tasks.
     *
     */
    private void purgetasks(Logging l) {
        synchronized (tasks) {
            for (Iterator it = tasks.iterator(); it.hasNext();) {
                I2PTunnelTask t = (I2PTunnelTask) it.next();
                if (!t.isOpen()) {
                    _log.debug(getPrefix() + "Purging inactive tunnel: [" + t.getId() + "] " + t.toString());
                    it.remove();
                }
            }
        }
    }

    /**
     * Log the given message (using both the logging subsystem and standard output...)
     *
     */
    public void log(String s) {
        System.out.println(s);
        _log.info(getPrefix() + "Display: " + s);
    }

    /**
     * Create a new destination, storing the destination and its private keys where 
     * instructed
     *
     * @param writeTo location to store the private keys
     * @param pubDest location to store the destination
     * @param l logger to send messages to
     */
    public static void makeKey(OutputStream writeTo, OutputStream pubDest, Logging l) {
        try {
            l.log("Generating new keys...");
            ByteArrayOutputStream priv = new ByteArrayOutputStream(), pub = new ByteArrayOutputStream();
            I2PClient client = I2PClientFactory.createClient();
            Destination d = client.createDestination(writeTo);
            l.log("Secret key saved.");
            l.log("Public key: " + d.toBase64());
            writeTo.flush();
            writeTo.close();
            writePubKey(d, pubDest, l);
        } catch (I2PException ex) {
            ex.printStackTrace();
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }

    /**
     * Read in the given destination, display it, and write it to the given location
     *
     * @param readFrom stream to read the destination from
     * @param pubDest stream to write the destination to
     * @param l logger to send messages to
     */
    public static void showKey(InputStream readFrom, OutputStream pubDest, Logging l) {
        try {
            I2PClient client = I2PClientFactory.createClient();
            Destination d = new Destination();
            d.readBytes(readFrom);
            l.log("Public key: " + d.toBase64());
            readFrom.close();
            writePubKey(d, pubDest, l);
        } catch (I2PException ex) {
            ex.printStackTrace();
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }

    /**
     * Write out the destination to the stream
     *
     * @param d Destination to write
     * @param o stream to write the destination to
     * @param l logger to send messages to
     */
    private static void writePubKey(Destination d, OutputStream o, Logging l) throws I2PException, IOException {
        if (o == null) return;
        d.writeBytes(o);
        l.log("Public key saved.");
    }

    /**
     * Generates a Destination from a name. Now only supports base64
     * names - may support naming servers later. "file:<filename>" is
     * also supported, where filename is a file that either contains a 
     * binary Destination structure or the Base64 encoding of that 
     * structure.
     */
    public static Destination destFromName(String name) throws DataFormatException {

        if ((name == null) || (name.trim().length() <= 0)) throw new DataFormatException("Empty destination provided");

        I2PAppContext ctx = I2PAppContext.getGlobalContext();
        Log log = ctx.logManager().getLog(I2PTunnel.class);
        
        if (name.startsWith("file:")) {
            Destination result = new Destination();
            byte content[] = null;
            FileInputStream in = null;
            try {
                in = new FileInputStream(name.substring("file:".length()));
                byte buf[] = new byte[1024];
                int read = DataHelper.read(in, buf);
                content = new byte[read];
                System.arraycopy(buf, 0, content, 0, read);
            } catch (IOException ioe) {
                System.out.println(ioe.getMessage());
                return null;
            } finally {
                if (in != null) try {
                    in.close();
                } catch (IOException io) {
                }
            }
            try {
                result.fromByteArray(content);
                return result;
            } catch (Exception ex) {
                if (log.shouldLog(Log.INFO)) 
                    log.info("File is not a binary destination - trying base64");
                try {
                    byte decoded[] = Base64.decode(new String(content));
                    result.fromByteArray(decoded);
                    return result;
                } catch (DataFormatException dfe) {
                    if (log.shouldLog(Log.WARN)) 
                        log.warn("File is not a base64 destination either - failing!");
                    return null;
                }
            }
        } else {
            // ask naming service
            NamingService inst = ctx.namingService();
            return inst.lookup(name);
        }
    }

    public void addConnectionEventListener(ConnectionEventListener lsnr) {
        if (lsnr == null) return;
        synchronized (listeners) {
            listeners.add(lsnr);
        }
    }

    public void removeConnectionEventListener(ConnectionEventListener lsnr) {
        if (lsnr == null) return;
        synchronized (listeners) {
            listeners.remove(lsnr);
        }
    }
    
    private String getPrefix() { return '[' + _tunnelId + "]: "; }
    
    public I2PAppContext getContext() { return _context; }

    /**
     * Call this whenever we lose touch with the router involuntarily (aka the router
     * is off / crashed / etc)
     *
     */
    void routerDisconnected() {
        _log.error(getPrefix() + "Router disconnected - firing notification events");
        synchronized (listeners) {
            for (Iterator iter = listeners.iterator(); iter.hasNext();) {
                ConnectionEventListener lsnr = (ConnectionEventListener) iter.next();
                if (lsnr != null) lsnr.routerDisconnected();
            }
        }
    }

    /**
     * Callback routine to find out
     */
    public interface ConnectionEventListener {
        public void routerDisconnected();
    }

    /* Required by the EventDispatcher interface */
    public EventDispatcher getEventDispatcher() {
        return _event;
    }

    public void attachEventDispatcher(EventDispatcher e) {
        _event.attachEventDispatcher(e.getEventDispatcher());
    }

    public void detachEventDispatcher(EventDispatcher e) {
        _event.detachEventDispatcher(e.getEventDispatcher());
    }

    public void notifyEvent(String e, Object a) {
        _event.notifyEvent(e, a);
    }

    public Object getEventValue(String n) {
        return _event.getEventValue(n);
    }

    public Set getEvents() {
        return _event.getEvents();
    }

    public void ignoreEvents() {
        _event.ignoreEvents();
    }

    public void unIgnoreEvents() {
        _event.unIgnoreEvents();
    }

    public Object waitEventValue(String n) {
        return _event.waitEventValue(n);
    }
}
