/*
 * 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.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.lang.reflect.Constructor;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicLong;

import gnu.getopt.Getopt;
import gnu.getopt.LongOpt;

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.I2PSessionException;
import net.i2p.client.I2PSimpleClient;
import net.i2p.client.naming.NamingService;
import net.i2p.crypto.Blinding;
import net.i2p.crypto.EncType;
import net.i2p.data.Base64;
import net.i2p.data.BlindData;
import net.i2p.data.DataFormatException;
import net.i2p.data.DataHelper;
import net.i2p.data.Destination;
import net.i2p.data.PrivateKey;
import net.i2p.i2ptunnel.socks.I2PSOCKSIRCTunnel;
import net.i2p.i2ptunnel.socks.I2PSOCKSTunnel;
import net.i2p.i2ptunnel.streamr.StreamrConsumer;
import net.i2p.i2ptunnel.streamr.StreamrProducer;
import net.i2p.util.EventDispatcherImpl;
import net.i2p.util.Log;
import net.i2p.util.OrderedProperties;

/**
 *  An I2PTunnel tracks one or more I2PTunnelTasks and one or more I2PSessions.
 *  Usually one of each.
 *
 *  TODO: Most events are not listened to elsewhere, so error propagation is poor
 */
public class I2PTunnel extends EventDispatcherImpl implements Logging {
    private final Log _log;
    private final I2PAppContext _context;
    private static final AtomicLong __tunnelId = new AtomicLong();
    private final long _tunnelId;
    private final Properties _clientOptions;
    private final Set<I2PSession> _sessions;
    private final TunnelController _controller;

    public static final int PACKET_DELAY = 100;

    public boolean ownDest = false;

    /** the I2CP port, non-null */
    public String port = System.getProperty(I2PClient.PROP_TCP_PORT, Integer.toString(I2PClient.DEFAULT_LISTEN_PORT));
    /** the I2CP host, non-null */
    public String host = System.getProperty(I2PClient.PROP_TCP_HOST, "127.0.0.1");
    /** the listen-on host. Sadly the listen-on port does not have a field. */
    public String listenHost = host;

    public long readTimeout = -1;

    /**
     *  Absolute path to filter definition file
     *  @since 0.9.40
     */
    public String filterDefinition;

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

    private final List<I2PTunnelTask> tasks = new CopyOnWriteArrayList<I2PTunnelTask>();
    private int next_task_id = 1;

    private final Set<ConnectionEventListener> listeners = new CopyOnWriteArraySet<ConnectionEventListener>();

    private static final int NOGUI = 99999;
    private static final LongOpt[] longopts = new LongOpt[] {
        new LongOpt("cli", LongOpt.NO_ARGUMENT, null, 'c'),
        new LongOpt("die", LongOpt.NO_ARGUMENT, null, 'd'),
        new LongOpt("gui", LongOpt.NO_ARGUMENT, null, 'g'),
        new LongOpt("help", LongOpt.NO_ARGUMENT, null, 'h'),
        new LongOpt("nocli", LongOpt.NO_ARGUMENT, null, 'w'),
        new LongOpt("nogui", LongOpt.NO_ARGUMENT, null, NOGUI),
        new LongOpt("wait", LongOpt.NO_ARGUMENT, null, 'w')
    };

    /** @since 0.9.17 */
    private enum CloseMode { NORMAL, FORCED, DESTROY }

    public static void main(String[] args) {
        try {
            new I2PTunnel(args);
        } catch (IllegalArgumentException iae) {
            System.err.println(iae.toString());
            System.exit(1);
        }
    }

    /**
     *  Standard constructor for embedded, uses args "-nocli -die" to return immediately
     */
    public I2PTunnel() {
        this(nocli_args);
    }

    /**
     *  New standard constructor in router, with back ref to tc
     *  @param tc may be null
     *  @throws IllegalArgumentException
     *  @since 0.9.48
     */
    public I2PTunnel(TunnelController tc) {
        this(nocli_args, null, tc);
    }

    /**
     *  See usage() for options
     *  @throws IllegalArgumentException
     */
    public I2PTunnel(String[] args) {
        this(args, null);
    }

    /**
     *  See usage() for options
     *  @param lsnr may be null
     *  @throws IllegalArgumentException
     */
    public I2PTunnel(String[] args, ConnectionEventListener lsnr) {
        this(args, lsnr, null);
    }

    /**
     *  @param lsnr may be null
     *  @param tc may be null
     *  @throws IllegalArgumentException
     *  @since 0.9.48
     */
    private I2PTunnel(String[] args, ConnectionEventListener lsnr, TunnelController tc) {
        super();
        _context = I2PAppContext.getGlobalContext(); // new I2PAppContext();
        _tunnelId = __tunnelId.incrementAndGet();
        _log = _context.logManager().getLog(I2PTunnel.class);
        _controller = tc;
        // as of 0.8.4, include context properties
        Properties p = _context.getProperties();
        _clientOptions = p;
        _sessions = new CopyOnWriteArraySet<I2PSession>();
        
        addConnectionEventListener(lsnr);
        boolean gui = true;
        boolean checkRunByE = true;
        boolean cli = true;
        boolean dontDie = true;
        boolean error = false;
        List<String> eargs = null;
        Getopt g = new Getopt("i2ptunnel", args, "d::n:c::w::e:h::", longopts);
        int c;
        while ((c = g.getopt()) != -1) {
          switch (c) {
            case 'd':  // -d, -die, --die
                dontDie = false;
                gui = false;
                cli = false;
                checkRunByE = false;
                break;

            case 'n':  // -noc, -nog, -nocli, -nogui
                String a = g.getOptarg();
                if (a.startsWith("oc")) {
                    gui = false;
                    cli = false;
                    checkRunByE = false;
                    break;
                } else if (a.startsWith("og")) {
                    // fall thru
                } else {
                    error = true;
                    break;
                }
                // fall thru for -nogui only

            case NOGUI:  // --nogui
                gui = false;
                if (_log.shouldLog(Log.WARN))
                    _log.warn(getPrefix() + "The `-nogui' option of I2PTunnel is deprecated.\n"
                          + "Use `-cli', `-nocli' (aka `-wait') or `-die' instead.");

            case 'c':  // -c, -cli, --cli
                gui = false;
                cli = true;
                checkRunByE = false;
                break;

            case 'w':  // -w, -wait, --nocli
                gui = false;
                cli = false;
                checkRunByE = false;
                break;

            case 'e':
                if (eargs == null)
                    eargs = new ArrayList<String>(4);
                eargs.add(g.getOptarg());
                if (checkRunByE) {
                    checkRunByE = false;
                    cli = false;
                }
                break;

            case 'h':
            case '?':
            case ':':
            default:
              error = true;
          }
        }

        int remaining = args.length - g.getOptind();

        if (error || remaining > 1) {
            System.err.println(usage());
            throw new IllegalArgumentException();
        }

        if (eargs != null) {
            for (String arg : eargs) {
                runCommand(arg, this);
            }
        }

        if (remaining == 1) {
            String f = args[g.getOptind()];
            File file = new File(f);
            // This is probably just a problem with the options, so
            // throw from here
            if (!file.exists()) {
                System.err.println(usage());
                throw new IllegalArgumentException("Command file does not exist: " + f);
            }
            runCommand("run " + f, this);
        }

        if (gui) {
            // removed from source, now in i2p.scripts
            //new I2PTunnelGUI(this);
            try {
                Class<?> cls = Class.forName("net.i2p.i2ptunnel.I2PTunnelGUI");
                Constructor<?> con = cls.getConstructor(I2PTunnel.class);
                con.newInstance(this);
            } catch (Throwable t) {
                throw new UnsupportedOperationException("GUI is not available, try -cli", t);
            }
        } 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;
                    if (cmd.length() <= 0) continue;
                    try {
                        runCommand(cmd, this);
                    } catch (Throwable t) {
                        t.printStackTrace();
                    }
                }
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        } else if (eargs == null && remaining == 0 && dontDie) {
            System.err.println(usage());
            throw new IllegalArgumentException("Waiting for nothing! Specify gui, cli, command, command file, or die");
        }

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

    /** with newlines except for last line */
    private static String usage() {
        // not sure this all makes sense, just documenting what's above
        return
            "Usage: i2ptunnel [options] [commandFile]\n" +
            "  Default is to run the GUI.\n" +
            "  commandFile: run all commands in this file\n" +
            "  Options:\n" +
            "    -c, -cli, --cli     :  run the command line interface\n" +
            "    -d, -die, --die     :  exit immediately, do not wait for commands to finish\n" +
            "    -e 'command [args]' :  run the command\n" +
            "    -h, --help          :  display this help\n" +
            "    -nocli, --nocli     :  do not run the command line interface or GUI\n" +
            "    -nogui, --nogui     :  do not run the GUI\n" +
            "    -w, -wait, --wait   :  do not run the command line interface or GUI";
    }

    /**
     *  @return A copy, unmodifiable, non-null
     *  @since public since 0.9.53 for advanced plugin usage, was package private
     */
    public List<I2PSession> getSessions() {
        if (_sessions.isEmpty())
            return Collections.emptyList();
        return new ArrayList<I2PSession>(_sessions); 
    }

    /**
     *  @param session null ok
     */
    void addSession(I2PSession session) { 
        if (session == null) return;
        boolean added = _sessions.add(session);
        if (added && _log.shouldLog(Log.INFO))
            _log.info(getPrefix() + " session added: " + session, new Exception());
    }

    /**
     *  @param session null ok
     */
    void removeSession(I2PSession session) { 
        if (session == null) return;
        boolean removed = _sessions.remove(session);
        if (removed && _log.shouldLog(Log.INFO))
            _log.info(getPrefix() + " session removed: " + session, new Exception());
    }
    
    /**
     *  Generic options used for clients and servers.
     *  NOT a copy, Do NOT modify for per-connection options, make a copy.
     *  @return non-null, NOT a copy, do NOT modify for per-connection options
     */
    public Properties getClientOptions() { return _clientOptions; }
    
    /**
     *  TunnelController that constructed this, or null.
     *  @return controller or null
     *  @since 0.9.48
     */
    TunnelController getController() { return _controller; }
    
    private void addtask(I2PTunnelTask tsk) {
        tsk.setTunnel(this);
        if (tsk.isOpen()) {
            tsk.setId(next_task_id);
            next_task_id++;
            tasks.add(tsk);
            if (_log.shouldLog(Log.INFO))
                _log.info(getPrefix() + " adding task: " + tsk);
        } else {
            if (_log.shouldLog(Log.INFO))
                _log.info(getPrefix() + " not adding task that isn't open: " + 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(Locale.US);
        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 ("httpbidirserver".equals(cmdname)) {
            runHttpBidirServer(args, l);
        } else if ("ircserver".equals(cmdname)) {
            runIrcServer(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 ("socksirctunnel".equals(cmdname)) {
            runSOCKSIRCTunnel(args, l);
        } else if ("connectclient".equals(cmdname)) {
            runConnectClient(args, l);
        } else if ("streamrclient".equals(cmdname)) {
            runStreamrClient(args, l);
        } else if ("streamrserver".equals(cmdname)) {
            runStreamrServer(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 if (cmdname.equals("auth")) {
            runAuth(args, l);
        } else if (cmdname.equals("blinding")) {
            runBlinding(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
     */
    private static void runHelp(Logging l) {
        l.log("Command list:\n" +
        // alphabetical please...
              "  auth <username> <password>\n" +
              "  blinding [-d|p] [-k key] [-s password] [-e expires] xxx.b32.i2p\n" +
              "  client <port> <pubkey>[,<pubkey,...]|file:<pubkeyfile> [<sharedClient>]\n" +
              "  clientoptions [-acx] [key=value ]*\n" +
              "  close [forced|destroy] <jobnumber>|all\n" +
              "  config [-s] <I2CPhost> <I2CPport>\n" +
              "  connectclient <port> [<sharedClient>] [<proxy>]\n" +
              "  genkeys <privkeyfile> [<pubkeyfile>]\n" +
              "  gentextkeys\n" +
              "  httpbidirserver <host> <port> <proxyport> <spoofedhost> <privkeyfile>\n" +
              "  httpclient <port> [<sharedClient>] [<proxy>]\n" +
              "  httpserver <host> <port> <spoofedhost> <privkeyfile>\n" +
              "  ircclient <port> <pubkey>[,<pubkey,...]|file:<pubkeyfile> [<sharedClient>]\n" +
              "  list\n" +
              "  listen_on <ip>\n" +
              "  lookup <name>\n" +
              "  owndest yes|no\n" +
              "  ping <args>\n" +
              "  quit\n" +
              "  read_timeout <msecs>\n" +
              "  run <commandfile>\n" +
              "  server <host> <port> <privkeyfile>\n" +
              "  socksirctunnel <port> [<sharedClient> [<privKeyFile>]]\n" +
              "  sockstunnel <port>\n" +
              "  streamrclient <host> <port> <destination>\n" +
              "  streamrserver <port> <privkeyfile>\n" +
              "  textserver <host> <port> <privkey>\n");
    }
    
    /**
     * Configure the extra I2CP options to use in any subsequent I2CP sessions.
     * Generic options used for clients and servers
     * Usage: "clientoptions[ key=value]*" .  
     *
     * Sets the event "clientoptions_onResult" = "ok" after completion.
     *
     * Deprecated To be made private, use setClientOptions().
     * This does NOT update a running TunnelTask.
     *
     * @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) {
        if (args != null && args.length > 0) {
            int i = 0;
            if (args[0].equals("-a")) {
                i++;
            } else if (args[0].equals("-c")) {
                _clientOptions.clear();
                l.log("Client options cleared");
                return;
            } else if (args[0].equals("-x")) {
                i++;
                for ( ; i < args.length; i++) {
                     if (_clientOptions.remove(args[i]) != null)
                        l.log("Removed " + args[i]);
                }
                return;
            } else {
                _clientOptions.clear();
            }
            for ( ; 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);
            }
        } else {
            l.log("Usage:\n" +
                  "  clientoptions                   // show help and list current options\n" +
                  "  clientoptions [key=value ]*     // sets current options\n" +
                  "  clientoptions -a [key=value ]*  // adds to current options\n" +
                  "  clientoptions -c                // clears current options\n" +
                  "  clientoptions -x [key ]*        // removes listed options\n" +
                  "\nCurrent options:");
            Properties p = new OrderedProperties();
            p.putAll(_clientOptions);
            for (Map.Entry<Object, Object> e : p.entrySet()) {
                l.log("  [" + e.getKey() + "] = [" + e.getValue() + ']');
            }
        }
        notifyEvent("clientoptions_onResult", "ok");
    }

    /**
     * Generic options used for clients and servers.
     * This DOES update a running TunnelTask, but NOT the session.
     * A more efficient runClientOptions().
     *
     * Defaults in opts properties are not recommended, they may or may not be honored.
     *
     * @param opts non-null
     * @since 0.9.1
     */
    public void setClientOptions(Properties opts) {
        for (Iterator<Object> iter = _clientOptions.keySet().iterator(); iter.hasNext();) {
            Object key = iter.next();
            if (!opts.containsKey(key))
                iter.remove();
        }
        _clientOptions.putAll(opts);
        for (I2PTunnelTask task : tasks) {
            task.optionsUpdated(this);
        }
        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
     * @throws IllegalArgumentException on config problem
     */
    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", Integer.valueOf(-1));
                throw new IllegalArgumentException(getPrefix() + "Error resolving " + args[0] + uhe.getMessage());
            }

            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", Integer.valueOf(-1));
            }
            if (portNum <= 0)
                throw new IllegalArgumentException(getPrefix() + "Bad port " + args[1]);

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

    /**
     * Same args as runServer
     * (we should stop duplicating all this code...)
     * @throws IllegalArgumentException on config problem
     */
    public void runIrcServer(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", Integer.valueOf(-1));
                throw new IllegalArgumentException(getPrefix() + "Error resolving " + args[0] + uhe.getMessage());
            }

            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", Integer.valueOf(-1));
            }
            if (portNum <= 0)
                throw new IllegalArgumentException(getPrefix() + "Bad port " + args[1]);

            privKeyFile = new File(args[2]);
            if (!privKeyFile.isAbsolute())
                privKeyFile = new File(_context.getConfigDir(), args[2]);
            if (!privKeyFile.canRead()) {
                l.log(getPrefix() + "Private key file does not exist or is not readable: " + args[2]);
                _log.error(getPrefix() + "Private key file does not exist or is not readable: " + args[2]);
                notifyEvent("serverTaskId", Integer.valueOf(-1));
                throw new IllegalArgumentException(getPrefix() + "Cannot open private key file " + args[2]);
            }
            I2PTunnelServer serv = new I2PTunnelIRCServer(serverHost, portNum, privKeyFile, args[2], l, this, this);
            serv.setReadTimeout(readTimeout);
            serv.startRunning();
            addtask(serv);
            notifyEvent("serverTaskId", Integer.valueOf(serv.getId()));
            return;
        } else {
            l.log("server <host> <port> <privkeyfile>\n" +
                  "  creates a server that sends all incoming data\n" + "  of its destination to host:port.");
            notifyEvent("serverTaskId", Integer.valueOf(-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
     * @throws IllegalArgumentException on config problem
     */
    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", Integer.valueOf(-1));
                throw new IllegalArgumentException(getPrefix() + "Error resolving " + args[0] + uhe.getMessage());
            }

            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", Integer.valueOf(-1));
            }
            if (portNum <= 0)
                throw new IllegalArgumentException(getPrefix() + "Bad port " + args[1]);

            String spoofedHost = args[2];
            
            privKeyFile = new File(args[3]);
            if (!privKeyFile.isAbsolute())
                privKeyFile = new File(_context.getConfigDir(), args[3]);
            if (!privKeyFile.canRead()) {
                l.log(getPrefix() + "Private key file does not exist or is not readable: " + args[3]);
                _log.error(getPrefix() + "Private key file does not exist or is not readable: " + args[3]);
                notifyEvent("serverTaskId", Integer.valueOf(-1));
                throw new IllegalArgumentException(getPrefix() + "Cannot open private key file " + args[3]);
            }
            I2PTunnelHTTPServer serv = new I2PTunnelHTTPServer(serverHost, portNum, privKeyFile, args[3], spoofedHost, l, this, this);
            serv.setReadTimeout(readTimeout);
            serv.startRunning();
            addtask(serv);
            notifyEvent("serverTaskId", Integer.valueOf(serv.getId()));
            return;
        } else {
            l.log("httpserver <host> <port> <spoofedhost> <privkeyfile>\n" +
                  "  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", Integer.valueOf(-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. Also runs an HTTP proxy for
     * bidirectional communications on the same tunnel destination.<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, proxyPortNumber, spoofedHost, privKeyFilename}
     * @param l logger to receive events and output
     * @throws IllegalArgumentException on config problem
     */
    public void runHttpBidirServer(String args[], Logging l) {
        if (args.length == 5) {
            InetAddress serverHost = null;
            int portNum = -1;
            int port2Num = -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", Integer.valueOf(-1));
                throw new IllegalArgumentException(getPrefix() + "Error resolving " + args[0] + uhe.getMessage());
            }

            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", Integer.valueOf(-1));
            }

            try {
                port2Num = Integer.parseInt(args[2]);
            } catch (NumberFormatException nfe) {
                l.log("invalid port");
                _log.error(getPrefix() + "Port specified is not valid: " + args[2], nfe);
                notifyEvent("serverTaskId", Integer.valueOf(-1));
            }
            if (portNum <= 0)
                throw new IllegalArgumentException(getPrefix() + "Bad port " + args[1]);
            if (port2Num <= 0)
                throw new IllegalArgumentException(getPrefix() + "Bad port " + args[2]);

            String spoofedHost = args[3];

            privKeyFile = new File(args[4]);
            if (!privKeyFile.isAbsolute())
                privKeyFile = new File(_context.getConfigDir(), args[4]);
            if (!privKeyFile.canRead()) {
                l.log(getPrefix() + "Private key file does not exist or is not readable: " + args[4]);
                _log.error(getPrefix() + "Private key file does not exist or is not readable: " + args[4]);
                notifyEvent("serverTaskId", Integer.valueOf(-1));
                throw new IllegalArgumentException(getPrefix() + "Cannot open private key file " + args[4]);
            }

            I2PTunnelHTTPBidirServer serv = new I2PTunnelHTTPBidirServer(serverHost, portNum, port2Num, privKeyFile, args[3], spoofedHost, l, this, this);
            serv.setReadTimeout(readTimeout);
            serv.startRunning();
            addtask(serv);
            notifyEvent("serverTaskId", Integer.valueOf(serv.getId()));
            return;
        } else {
            l.log("httpserver <host> <port> <proxyport> <spoofedhost> <privkeyfile>\n" +
                  "  creates a bidirectional 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,"
                  + "  and listens to host:proxyport to proxy HTTP requests.");
            notifyEvent("serverTaskId", Integer.valueOf(-1));
        }
    }

    /**
     * Run the server pointing at the host and port specified using the private i2p
     * destination loaded from the given base64 stream. <p>
     *
     * Deprecated? Why run a server with a private destination?
     * Not available from the war GUI
     *
     * 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
     * @throws IllegalArgumentException on config problem
     */
    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", Integer.valueOf(-1));
                throw new IllegalArgumentException(getPrefix() + "Error resolving " + args[0] + uhe.getMessage());
            }

            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", Integer.valueOf(-1));
            }
            if (portNum <= 0)
                throw new IllegalArgumentException(getPrefix() + "Bad port " + args[1]);

            I2PTunnelServer serv = new I2PTunnelServer(serverHost, portNum, args[2], l, this, this);
            serv.setReadTimeout(readTimeout);
            serv.startRunning();
            addtask(serv);
            notifyEvent("serverTaskId", Integer.valueOf(serv.getId()));
        } else {
            l.log("textserver <host> <port> <privkey>\n" +
                  "  creates a server that sends all incoming data\n" + "  of its destination to host:port.");
            notifyEvent("textserverTaskId", Integer.valueOf(-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 [, privKeyFile]]}
     * @param l logger to receive events and output
     * @throws IllegalArgumentException on config problem
     */
    public void runClient(String args[], Logging l) {
        boolean isShared = true;
        if (args.length >= 3)
            isShared = Boolean.parseBoolean(args[2].trim());
        if (args.length >= 2) {
            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", Integer.valueOf(-1));
            }
            if (portNum <= 0)
                throw new IllegalArgumentException(getPrefix() + "Bad port " + args[0]);

            ownDest = !isShared;
            try {
                String privateKeyFile = null;
                if (args.length >= 4)
                    privateKeyFile = args[3];
                I2PTunnelClientBase task = new I2PTunnelClient(portNum, args[1], l, ownDest, this, this, privateKeyFile);
                task.startRunning();
                addtask(task);
                notifyEvent("clientTaskId", Integer.valueOf(task.getId()));
            } catch (IllegalArgumentException iae) {
                String msg = "Invalid I2PTunnel configuration to create a standard client tunnel connecting to the router at " + host + ':'+ port +
                             " and listening on " + listenHost + ':' + portNum;
                _log.error(getPrefix() + msg, iae);
                l.log(msg);
                notifyEvent("clientTaskId", Integer.valueOf(-1));
                // Since nothing listens to TaskID events, use this to propagate the error to TunnelController
                // Otherwise, the tunnel stays up even though the port is down
                // This doesn't work for CLI though... and the tunnel doesn't close itself after error,
                // so this probably leaves the tunnel open if called from the CLI
                throw iae;
            }
        } else {
            l.log("client <port> <pubkey>[,<pubkey>]|file:<pubkeyfile>[ <sharedClient>] [<privKeyFile>]\n" +
                    "  Creates a standard client that listens on the port and forwards to the pubkey.\n"
                  + "  With a comma delimited list of pubkeys, it will rotate among them randomly.\n"
                  + "  sharedClient indicates if this client shares tunnels with other clients (true or false)");
            notifyEvent("clientTaskId", Integer.valueOf(-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
     * @throws IllegalArgumentException on config problem
     */
    public void runHttpClient(String args[], Logging l) {
        if (args.length >= 1 && args.length <= 3) {
            int clientPort = -1;
            try {
                clientPort = Integer.parseInt(args[0]);
            } catch (NumberFormatException nfe) {
                l.log("invalid port");
                _log.error(getPrefix() + "Port specified is not valid: " + args[0], nfe);
                notifyEvent("httpclientTaskId", Integer.valueOf(-1));
            }
            if (clientPort <= 0)
                throw new IllegalArgumentException(getPrefix() + "Bad port " + args[0]);
            
            String proxy = "";
            boolean isShared = true;
            if (args.length > 1) {
                if (Boolean.parseBoolean(args[1].trim())) {
                    isShared = true;
                    if (args.length == 3)
                        proxy = args[2];
                } else if ("false".equalsIgnoreCase(args[1].trim())) {
                    isShared = false;
                    if (args.length == 3)
                        proxy = args[2];
                } else if (args.length == 3) {
                    isShared = false; // not "true"
                    proxy = args[2];
                } else {
                    // isShared not specified, default to true
                    isShared = true;
                    proxy = args[1];
                }
            }

            ownDest = !isShared;
            try {
                I2PTunnelClientBase task = new I2PTunnelHTTPClient(clientPort, l, ownDest, proxy, this, this);
                task.startRunning();
                addtask(task);
                notifyEvent("httpclientTaskId", Integer.valueOf(task.getId()));
            } catch (IllegalArgumentException iae) {
                String msg = "Invalid I2PTunnel configuration to create an HTTP Proxy connecting to the router at " + host + ':'+ port +
                             " and listening on " + listenHost + ':' + clientPort;
                _log.error(getPrefix() + msg, iae);
                l.log(msg);
                notifyEvent("httpclientTaskId", Integer.valueOf(-1));
                // Since nothing listens to TaskID events, use this to propagate the error to TunnelController
                // Otherwise, the tunnel stays up even though the port is down
                // This doesn't work for CLI though... and the tunnel doesn't close itself after error,
                // so this probably leaves the tunnel open if called from the CLI
                throw iae;
            }
        } else {
            l.log("httpclient <port> [<sharedClient>] [<proxy>]\n" +
                  "  Creates a HTTP client proxy on the specified port.\n" +
                  "  <sharedClient> (optional) Indicates if this client shares tunnels with other clients (true or false)\n" +
                  "  <proxy> (optional) Indicates a proxy server to be used\n" +
                  "  when trying to access an address out of the .i2p domain");
            notifyEvent("httpclientTaskId", Integer.valueOf(-1));
        }
    }

    /**
     * Run a CONNECT client on the given port number 
     *
     * @param args {portNumber[, sharedClient][, proxy to be used for the WWW]}
     * @param l logger to receive events and output
     * @throws IllegalArgumentException on config problem
     */
    public void runConnectClient(String args[], Logging l) {
        if (args.length >= 1 && args.length <= 3) {
            int _port = -1;
            try {
                _port = Integer.parseInt(args[0]);
            } catch (NumberFormatException nfe) {
                _log.error(getPrefix() + "Port specified is not valid: " + args[0], nfe);
            }
            if (_port <= 0)
                throw new IllegalArgumentException(getPrefix() + "Bad port " + args[0]);
            
            String proxy = "";
            boolean isShared = true;
            if (args.length > 1) {
                if (Boolean.parseBoolean(args[1].trim())) {
                    isShared = true;
                    if (args.length == 3)
                        proxy = args[2];
                } else if ("false".equalsIgnoreCase(args[1].trim())) {
                    isShared = false;
                    if (args.length == 3)
                        proxy = args[2];
                } else if (args.length == 3) {
                    isShared = false; // not "true"
                    proxy = args[2];
                } else {
                    // isShared not specified, default to true
                    isShared = true;
                    proxy = args[1];
                }
            }

            ownDest = !isShared;
            try {
                I2PTunnelClientBase task = new I2PTunnelConnectClient(_port, l, ownDest, proxy, this, this);
                task.startRunning();
                addtask(task);
            } catch (IllegalArgumentException iae) {
                String msg = "Invalid I2PTunnel configuration to create a CONNECT client connecting to the router at " + host + ':'+ port +
                             " and listening on " + listenHost + ':' + _port;
                _log.error(getPrefix() + msg, iae);
                l.log(msg);
                // Since nothing listens to TaskID events, use this to propagate the error to TunnelController
                // Otherwise, the tunnel stays up even though the port is down
                // This doesn't work for CLI though... and the tunnel doesn't close itself after error,
                // so this probably leaves the tunnel open if called from the CLI
                throw iae;
            }
        } else {
            l.log("connectclient <port> [<sharedClient>] [<proxy>]\n" +
                  "  creates a client that for SSL/HTTPS requests.\n" +
                  "  <sharedClient> (optional) indicates if this client shares tunnels with other clients (true or false)\n" +
                  "  <proxy> (optional) indicates a proxy server to be used\n" +
                  "  when trying to access an address out of the .i2p domain\n");
        }
    }

    /**
     * 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 [, privKeyFile]]}
     * @param l logger to receive events and output
     * @throws IllegalArgumentException on config problem
     */
    public void runIrcClient(String args[], Logging l) {
        if (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("ircclientTaskId", Integer.valueOf(-1));
            }
            if (_port <= 0)
                throw new IllegalArgumentException(getPrefix() + "Bad port " + args[0]);
            
            boolean isShared = true;
            if (args.length > 2) {
                if (Boolean.parseBoolean(args[2].trim())) {
                    isShared = true;
                } else if ("false".equalsIgnoreCase(args[2].trim())) {
                    isShared = false;
                } else {
                    // isShared not specified, default to true
                    isShared = true;
                }
            }

            ownDest = !isShared;
            try {
                String privateKeyFile = null;
                if (args.length >= 4)
                    privateKeyFile = args[3];
                I2PTunnelClientBase task = new I2PTunnelIRCClient(_port, args[1], l, ownDest, this, this, privateKeyFile);
                task.startRunning();
                addtask(task);
                notifyEvent("ircclientTaskId", Integer.valueOf(task.getId()));
            } catch (IllegalArgumentException iae) {
                String msg = "Invalid I2PTunnel configuration to create an IRC client connecting to the router at " + host + ':'+ port +
                             " and listening on " + listenHost + ':' + _port;
                _log.error(getPrefix() + msg, iae);
                l.log(msg);
                notifyEvent("ircclientTaskId", Integer.valueOf(-1));
                // Since nothing listens to TaskID events, use this to propagate the error to TunnelController
                // Otherwise, the tunnel stays up even though the port is down
                // This doesn't work for CLI though... and the tunnel doesn't close itself after error,
                // so this probably leaves the tunnel open if called from the CLI
                throw iae;
            }
        } else {
            l.log("ircclient <port> [<sharedClient> [<privKeyFile>]]\n" +
                  "  Creates an IRC client proxy on the specified port.\n" +
                  "  <sharedClient> (optional) Indicates if this client shares tunnels with other clients (true or false)\n");
            notifyEvent("ircclientTaskId", Integer.valueOf(-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 [, sharedClient]} or (portNumber, ignored (false), privKeyFile)
     * @param l logger to receive events and output
     * @throws IllegalArgumentException on config problem
     */
    public void runSOCKSTunnel(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("sockstunnelTaskId", Integer.valueOf(-1));
            }
            if (_port <= 0)
                throw new IllegalArgumentException(getPrefix() + "Bad port " + args[0]);

            boolean isShared = false;
            if (args.length == 2)
                isShared = Boolean.parseBoolean(args[1].trim());

            ownDest = !isShared;
            String privateKeyFile = null;
            if (args.length == 3)
                privateKeyFile = args[2];
            try {
                I2PTunnelClientBase task = new I2PSOCKSTunnel(_port, l, ownDest, this, this, privateKeyFile);
                task.startRunning();
                addtask(task);
                notifyEvent("sockstunnelTaskId", Integer.valueOf(task.getId()));
            } catch (IllegalArgumentException iae) {
                String msg = "Invalid I2PTunnel configuration to create a SOCKS Proxy connecting to the router at " + host + ':'+ port +
                             " and listening on " + listenHost + ':' + _port;
                _log.error(getPrefix() + msg, iae);
                l.log(msg);
                notifyEvent("sockstunnelTaskId", Integer.valueOf(-1));
                throw iae;
            }
        } else {
            l.log("sockstunnel <port>\n" +
                  "  Creates a SOCKS proxy on the specified port.");
            notifyEvent("sockstunnelTaskId", Integer.valueOf(-1));
        }
    }

    
    /**
     * Run an SOCKS IRC tunnel on the given port number 
     * @param args {portNumber [, sharedClient]} or (portNumber, ignored (false), privKeyFile)
     * @throws IllegalArgumentException on config problem
     * @since 0.7.12
     */
    public void runSOCKSIRCTunnel(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("sockstunnelTaskId", Integer.valueOf(-1));
            }
            if (_port <= 0)
                throw new IllegalArgumentException(getPrefix() + "Bad port " + args[0]);

            boolean isShared = false;
            if (args.length == 2)
                isShared = Boolean.parseBoolean(args[1].trim());

            ownDest = !isShared;
            String privateKeyFile = null;
            if (args.length == 3)
                privateKeyFile = args[2];
            try {
                I2PTunnelClientBase task = new I2PSOCKSIRCTunnel(_port, l, ownDest, this, this, privateKeyFile);
                task.startRunning();
                addtask(task);
                notifyEvent("sockstunnelTaskId", Integer.valueOf(task.getId()));
            } catch (IllegalArgumentException iae) {
                String msg = "Invalid I2PTunnel configuration to create a SOCKS IRC Proxy connecting to the router at " + host + ':'+ port +
                             " and listening on " + listenHost + ':' + _port;
                _log.error(getPrefix() + msg, iae);
                l.log(msg);
                notifyEvent("sockstunnelTaskId", Integer.valueOf(-1));
                throw iae;
            }
        } else {
            l.log("socksirctunnel <port> [<sharedClient> [<privKeyFile>]]\n" +
                  "  Creates a SOCKS IRC proxy on the specified port.");
            notifyEvent("sockstunnelTaskId", Integer.valueOf(-1));
        }
    }

    /**
     * Streamr client
     *
     * @param args {targethost, targetport, destinationString}
     * @param l logger to receive events and output
     * @throws IllegalArgumentException on config problem
     */
    public void runStreamrClient(String args[], Logging l) {
        if (args.length == 3) {
            InetAddress _host;
            try {
                _host = InetAddress.getByName(args[0]);
            } catch (UnknownHostException uhe) {
                l.log("unknown host");
                _log.error(getPrefix() + "Error resolving " + args[0], uhe);
                notifyEvent("streamrtunnelTaskId", Integer.valueOf(-1));
                return;
            }

            int _port = -1;
            try {
                _port = Integer.parseInt(args[1]);
            } catch (NumberFormatException nfe) {
                l.log("invalid port");
                _log.error(getPrefix() + "Port specified is not valid: " + args[0], nfe);
                notifyEvent("streamrtunnelTaskId", Integer.valueOf(-1));
            }
            if (_port <= 0)
                throw new IllegalArgumentException(getPrefix() + "Bad port " + args[0]);

            try {
                StreamrConsumer task = new StreamrConsumer(_host, _port, args[2], l, this, this);
                task.startRunning();
                addtask(task);
                notifyEvent("streamrtunnelTaskId", Integer.valueOf(task.getId()));
            } catch (IllegalArgumentException iae) {
                String msg = "Invalid I2PTunnel configuration to create a Streamr Client connecting to the router at " + host + ':'+ port +
                             " and sending to " + _host + ':' + _port;
                _log.error(getPrefix() + msg, iae);
                l.log(msg);
                notifyEvent("streamrtunnnelTaskId", Integer.valueOf(-1));
                throw iae;
            }
        } else {
            l.log("streamrclient <host> <port> <destination>\n" +
                  "  creates a tunnel that receives streaming data.");
            notifyEvent("streamrtunnelTaskId", Integer.valueOf(-1));
        }
    }

    /**
     * Streamr server
     *
     * @param args {port, privkeyfile}
     * @param l logger to receive events and output
     * @throws IllegalArgumentException on config problem
     */
    public void runStreamrServer(String args[], Logging l) {
        if (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("streamrtunnelTaskId", Integer.valueOf(-1));
            }
            if (_port <= 0)
                throw new IllegalArgumentException(getPrefix() + "Bad port " + args[0]);

            File privKeyFile = new File(args[1]);
            if (!privKeyFile.isAbsolute())
                privKeyFile = new File(_context.getConfigDir(), args[1]);
            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", Integer.valueOf(-1));
                return;
            }

            StreamrProducer task = new StreamrProducer(_port, privKeyFile, args[1], l, this, this);
            task.startRunning();
            addtask(task);
            notifyEvent("streamrtunnelTaskId", Integer.valueOf(task.getId()));
        } else {
            l.log("streamrserver <port> <privkeyfile>\n" +
                  "  creates a tunnel that sends streaming data.");
            notifyEvent("streamrtunnelTaskId", Integer.valueOf(-1));
        }
    }

    /**
     * Specify the i2cp host and port 
     * Deprecated - only used by CLI
     *
     * 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
     */
    private void runConfig(String args[], Logging l) {
        if (args.length >= 1) {
            int i = 0;
            boolean ssl = args[0].equals("-s");
            if (ssl) {
                _clientOptions.setProperty("i2cp.SSL", "true");
                i++;
            } else {
                _clientOptions.remove("i2cp.SSL");
            }
            if (i < args.length) {
                host = args[i++];
            }
            if (i < args.length)
                port = args[i];
            l.log("New I2CP settings: " + host + ' ' + port + (ssl ? " SSL" : " non-SSL"));
            notifyEvent("configResult", "ok");
        } else {
            boolean ssl = Boolean.parseBoolean(_clientOptions.getProperty("i2cp.SSL"));
            l.log("Usage:\n" +
                  "  config [-s] [<i2phost>] [<i2pport>]\n" +
                  "  Sets the address and port of the I2P router.\n" +
                  "  Use -s for SSL.\n" +
                  "Current I2CP settings: " + host + ' ' + port + (ssl ? " SSL" : " non-SSL"));
            notifyEvent("configResult", "error");
        }
    }

    /**
     * Specify the i2cp username and password
     *
     * @param args {username, password}
     * @param l logger to receive events and output
     * @since 0.9.11
     */
    private void runAuth(String args[], Logging l) {
        if (args.length == 2) {
            _clientOptions.setProperty(I2PClient.PROP_USER, args[0]);
            _clientOptions.setProperty(I2PClient.PROP_PW, args[1]);
        } else {
            l.log("Usage:\n" +
                  "  auth <username> <password>\n" +
                  "  Sets the i2cp credentials");
        }
    }

    /**
     * Specify whether to use its own destination for each outgoing tunnel
     * Deprecated - only used by CLI
     *
     * 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
     */
    private 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\n" +
                  "  Specifies whether to use its own destination \n" + "  for each outgoing tunnel\n" +
                  "Current setting: " + (ownDest ? "yes" : "no (shared client)"));
            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>\n" +
                  "  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>\n" +
                  "  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.
     * Does NOT support non-default sig types.
     * Deprecated - only used by CLI
     *
     * 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
     */
    private static 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>]\n" +
                  "   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);
        } finally {
            if(pubdest != null) try { pubdest.close(); } catch(IOException ioe) {}
        }
    }

    /**
     * Generate a new keypair.
     * Does NOT support non-default sig types.
     * Deprecated - only used by CLI
     *
     * 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
     */
    private static void runGenTextKeys(Logging l) {
        ByteArrayOutputStream privkey = new ByteArrayOutputStream(1024);
        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.
     * Deprecated - only used by CLI
     *
     * 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
     */
    private void runQuit(Logging l) {
        purgetasks(l);
        if (tasks.isEmpty()) {
            System.exit(0);
        }
        l.log("There are running tasks. Try 'list' or 'close all'.");
        //notifyEvent("quitResult", "error");
    }

    /**
     * Retrieve a list of currently running tasks
     * Deprecated - only used by CLI
     *
     * Sets the event "listDone" = "done" after dumping the tasks to
     * the logger
     *
     * @param l logger to receive events and output
     */
    private void runList(Logging l) {
        purgetasks(l);
        for (I2PTunnelTask t : tasks) {
            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"}, {"destroy", jobNumber}, {"destroy", "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|destroy] <jobnumber>|all\n" +
                  "   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
            CloseMode mode = CloseMode.NORMAL;
            if (args[argindex].equalsIgnoreCase("forced")) {
                mode = CloseMode.FORCED;
                argindex++;
            } else if (args[argindex].equalsIgnoreCase("destroy")) {
                mode = CloseMode.DESTROY;
                argindex++;
            }
            if (args[argindex].equalsIgnoreCase("all")) {
                boolean error = false;
                if (tasks.isEmpty()) {
                    if (_log.shouldLog(Log.INFO))
                        _log.info(getPrefix() + " runClose(all) no tasks");
                }
                for (I2PTunnelTask t : tasks) {
                    if (!closetask(t, mode, 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]), mode, 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)
     * Deprecated - only used by CLI
     *
     * Sets the event "runResult" = "ok" or "error" after the closing is complete
     *
     * @param args {filename}
     * @param l logger to receive events and output
     */
    private void runRun(String args[], Logging l) {
        if (args.length == 1) {
            BufferedReader br = null;
            try {
                br = new BufferedReader(new InputStreamReader(new FileInputStream(args[0]), "UTF-8"));
                String line;
                while ((line = br.readLine()) != null) {
                    if (line.startsWith("#"))
                        continue;
                    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");
            } finally {
                if (br != null) try { br.close(); } catch (IOException ioe) {}
            }
        } else {
            l.log("run <commandfile>\n" +
                  "   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
     * Deprecated - only used by CLI
     *
     * Sets the event "lookupResult" = base64 of the destination, or an error message 
     *
     * @param args {name}
     * @param l logger to receive events and output
     */
    private void runLookup(String args[], Logging l) {
        if (args.length != 1) {
            l.log("lookup <name>\n" +
                  "   try to resolve the name into a destination key");
            notifyEvent("lookupResult", "invalidUsage");
        } else {
            try {
                boolean ssl = Boolean.parseBoolean(_clientOptions.getProperty(I2PClient.PROP_ENABLE_SSL));
                String user = _clientOptions.getProperty(I2PClient.PROP_USER);
                String pw = _clientOptions.getProperty(I2PClient.PROP_PW);
                Destination dest = destFromName(args[0], host, port, ssl, user, pw);
                if (dest == null) {
                    l.log("Unknown host: " + args[0]);
                    notifyEvent("lookupResult", "unknown host");
                } else {
                    l.log(dest.toBase64());
                    notifyEvent("lookupResult", dest.toBase64());
                }
            } catch (DataFormatException dfe) {
                l.log("Unknown or invalid host: " + args[0]);
                notifyEvent("lookupResult", "invalid host");
            }
        }
    }

    /**
     * Start up a ping task with the specified args (currently supporting -ns, -h, -l)
     * Deprecated - only used by CLI
     *
     * 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
     */
    private void runPing(String allargs, Logging l) {
        if (allargs.length() != 0) {
            _clientOptions.setProperty(I2Ping.PROP_COMMAND, allargs);
            if (ownDest) {
                if (!_clientOptions.containsKey("inbound.nickname"))
                    _clientOptions.setProperty("inbound.nickname", "I2Ping");
                if (!_clientOptions.containsKey("outbound.nickname"))
                    _clientOptions.setProperty("outbound.nickname", "I2Ping");
                if (!_clientOptions.containsKey("inbound.quantity"))
                    _clientOptions.setProperty("inbound.quantity", "1");
                if (!_clientOptions.containsKey("outbound.quantity"))
                    _clientOptions.setProperty("outbound.quantity", "1");
            }
            I2PTunnelClientBase task = new I2Ping(l, ownDest, this, this);
            task.startRunning();
            addtask(task);
            notifyEvent("pingTaskId", Integer.valueOf(task.getId()));
        } else {
            l.log(I2Ping.usage());
            notifyEvent("pingTaskId", Integer.valueOf(-1));
        }
    }

    /**
     * Send a BlindingInfo message, just for testing
     * @since 0.9.43
     */
    private void runBlinding(String[] argv, Logging l) {
        // blinding [-d|p] [-k key] [-s password] [-e expires] xxx.b32.i2p  // -d for DH; -p for PSK; expires in days
        Getopt g = new Getopt("blinding", argv, "dpk:s:e:");
        boolean error = false;
        boolean dh = false;
        boolean psk = false;
        String key = null;
        String pw = null;
        long expires = 0;
        int c;
        while ((c = g.getopt()) != -1) {
          switch (c) {
            case 'd':
              dh = true;
              break;

            case 'p':
              psk = true;
              break;

            case 'k':
              key = g.getOptarg();
              break;

            case 's':
              pw = g.getOptarg();
              break;

            case 'e':
              expires = Long.parseLong(g.getOptarg());
              break;

            case '?':
            case ':':
            default:
              error = true;
          }
        }
        int remaining = argv.length - g.getOptind();
        if (error || remaining != 1 || (dh && psk) || ((dh || psk) && key == null)) {
            System.out.println("Usage: blinding [-d|p] [-k key] [-s password] [-e expires] xxx.b32.i2p  // -d for DH; -p for PSK; expires in days");
            return;
        }
        String name = argv[g.getOptind()];
        BlindData bd;
        try {
            int auth = BlindData.AUTH_NONE;
            PrivateKey pk = null;
            if (key != null) {
                byte[] data = Base64.decode(key);
                if (data == null || data.length != 32) {
                    System.out.println("Invalid private key");
                    return;
                }
                pk = new PrivateKey(EncType.ECIES_X25519, data);
                auth = dh ? BlindData.AUTH_DH : BlindData.AUTH_PSK;
            }
            // basic check and decode
            bd = Blinding.decode(_context, name);
            // fill in key and secret
            bd = new BlindData(_context, bd.getUnblindedPubKey(), bd.getBlindedSigType(), pw, auth, pk);
            long now = _context.clock().now();
            bd.setDate(now);
            if (expires > 0) {
                expires *= 24*60*60*1000L;
                expires += now;
                bd.setExpiration(expires);
            }
        } catch (IllegalArgumentException iae) {
            System.out.println("Invalid b32 " + name + " - " + iae);
            return;
        }
        boolean ssl = Boolean.parseBoolean(_clientOptions.getProperty(I2PClient.PROP_ENABLE_SSL));
        String user = _clientOptions.getProperty(I2PClient.PROP_USER);
        String ipw = _clientOptions.getProperty(I2PClient.PROP_PW);
        I2PClient client = new I2PSimpleClient();
        Properties opts = new Properties();
        opts.put(I2PClient.PROP_TCP_HOST, host);
        opts.put(I2PClient.PROP_TCP_PORT, port);
        opts.put(I2PClient.PROP_ENABLE_SSL, Boolean.toString(ssl));
        if (user != null)
            opts.put(I2PClient.PROP_USER, user);
        if (ipw != null)
            opts.put(I2PClient.PROP_PW, ipw);
        I2PSession session = null;
        try {
            session = client.createSession(null, opts);
            session.connect();
            System.out.println("Sending: " + bd);
            session.sendBlindingInfo(bd);
            try { Thread.sleep(1000); } catch (InterruptedException ie) {}
        } catch (I2PSessionException ise) {
            System.out.println("Send blinding info failed: " + ise);
        } finally {
            if (session != null) {
                try { session.destroySession(); } catch (I2PSessionException ise) {}
            }
        }
    }

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

        _log.debug(getPrefix() + "closetask(): looking for task " + num);
            for (I2PTunnelTask t : tasks) {
                int id = t.getId();
                if (_log.shouldLog(Log.DEBUG))
                    _log.debug(getPrefix() + "closetask(): parsing task " + id + " (" + t.toString() + ")");
                if (id == num) {
                    closed = closetask(t, mode, 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, CloseMode mode, Logging l) {
        if (_log.shouldLog(Log.INFO))
            _log.info("Closing task " + t.getId() + " mode: " + mode);
        //l.log("Closing task " + t.getId() + (forced ? " forced..." : "..."));
        boolean success;
        if (mode == CloseMode.NORMAL)
            success = t.close(false);
        else if (mode == CloseMode.FORCED)
            success = t.close(true);
        else  // DESTROY
            success = t.destroy();
        if (success) {
            if (_log.shouldLog(Log.INFO))
                _log.info("Task " + t.getId() + " closed.");
            //l.log("Task " + t.getId() + " closed.");
        }
        return success;
    }

    /**
     * Helper task to remove closed / completed tasks.
     *
     */
    private void purgetasks(Logging l) {
            List<I2PTunnelTask> removed = new ArrayList<I2PTunnelTask>();
            for (I2PTunnelTask t : tasks) {
                if (!t.isOpen()) {
                    _log.debug(getPrefix() + "Purging inactive tunnel: [" + t.getId() + "] " + t.toString());
                    removed.add(t);
                }
            }
            tasks.removeAll(removed);
    }

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

    /**
     * Create a new destination, storing the destination and its private keys where 
     * instructed.
     * Does NOT support non-default sig types.
     * Deprecated - only used by CLI
     *
     * @param writeTo location to store the destination and private keys
     * @param pubDest location to store the destination
     * @param l logger to send messages to
     */
    private static void makeKey(OutputStream writeTo, OutputStream pubDest, Logging l) {
        try {
            l.log("Generating new keys...");
            I2PClient client = I2PClientFactory.createClient();
            Destination d = client.createDestination(writeTo);
            l.log("New destination: " + d.toBase32());
            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
     * Deprecated - only used by CLI
     *
     * @param readFrom stream to read the destination from
     * @param pubDest stream to write the destination to
     * @param l logger to send messages to
     */
    private static void showKey(InputStream readFrom, OutputStream pubDest, Logging l) {
        try {
            Destination d = new Destination();
            d.readBytes(readFrom);
            l.log("Destination: " + d.toBase32());
            readFrom.close();
            writePubKey(d, pubDest, l);
        } catch (I2PException ex) {
            ex.printStackTrace();
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }

    /**
     * Write out the destination to the stream
     * Deprecated - only used by CLI
     *
     * @param d Destination to write
     * @param o stream to write the destination to, or null for noop
     * @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:&lt;filename&gt;" is
     * also supported, where filename is a file that either contains a 
     * binary Destination structure or the Base64 encoding of that 
     * structure.
     *
     * Since file:&lt;filename&gt; isn't really used, this method is deprecated,
     * just call context.namingService.lookup() directly.
     * @deprecated Don't use i2ptunnel for lookup! Use I2PAppContext.getGlobalContext().namingService().lookup(name) from i2p.jar
     */
    @Deprecated
    public static Destination destFromName(String name) throws DataFormatException {
        return destFromName(name, null, null, false, null, null);
    }

    /**
     *  @param i2cpHost may be null
     *  @param i2cpPort may be null
     *  @param user may be null
     *  @param pw may be null
     *  @since 0.9.11
     */
    private static Destination destFromName(String name, String i2cpHost,
                                            String i2cpPort, boolean isSSL,
                                            String user, String pw) 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 (RuntimeException 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
            name = name.trim();
            NamingService inst = ctx.namingService();
            boolean b32 = name.length() == 60 && name.toLowerCase(Locale.US).endsWith(".b32.i2p");
            Destination d = null;
            if (ctx.isRouterContext() || !b32) {
                // Local lookup.
                // Even though we could do b32 outside router ctx here,
                // we do it below instead so we can set the host and port,
                // which we can't do with lookup()
                d = inst.lookup(name);
                if (d != null || ctx.isRouterContext() || name.length() >= 516)
                    return d;
            }
            // Outside router context only,
            // try simple session to ask the router.
            I2PClient client = new I2PSimpleClient();
            Properties opts = new Properties();
            if (i2cpHost != null)
                opts.put(I2PClient.PROP_TCP_HOST, i2cpHost);
            if (i2cpPort != null)
                opts.put(I2PClient.PROP_TCP_PORT, i2cpPort);
            opts.put(I2PClient.PROP_ENABLE_SSL, Boolean.toString(isSSL));
            if (user != null)
                opts.put(I2PClient.PROP_USER, user);
            if (pw != null)
                opts.put(I2PClient.PROP_PW, pw);
            I2PSession session = null;
            try {
                session = client.createSession(null, opts);
                session.connect();
                d = session.lookupDest(name);
            } catch (I2PSessionException ise) {
                if (log.shouldLog(Log.WARN)) 
                    log.warn("Lookup via router failed", ise);
            } finally {
                if (session != null) {
                    try { session.destroySession(); } catch (I2PSessionException ise) {}
                }
            }
            return d;
        }
    }

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

    public void removeConnectionEventListener(ConnectionEventListener lsnr) {
        if (lsnr == null) return;
        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");
            for (ConnectionEventListener lsnr : listeners) {
                if (lsnr != null) lsnr.routerDisconnected();
            }
    }

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