package net.i2p.i2ptunnel;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

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.crypto.KeyGenerator;
import net.i2p.crypto.SigType;
import net.i2p.data.Destination;
import net.i2p.data.KeyCertificate;
import net.i2p.data.PrivateKey;
import net.i2p.data.PrivateKeyFile;
import net.i2p.data.PublicKey;
import net.i2p.data.SigningPrivateKey;
import net.i2p.data.SigningPublicKey;
import net.i2p.data.SimpleDataStructure;
import net.i2p.i2ptunnel.socks.I2PSOCKSTunnel;
import net.i2p.util.FileUtil;
import net.i2p.util.I2PAppThread;
import net.i2p.util.Log;
import net.i2p.util.RandomSource;
import net.i2p.util.SecureFile;
import net.i2p.util.SecureFileOutputStream;
import net.i2p.util.SystemVersion;

/**
 * Coordinate the runtime operation and configuration of a single I2PTunnel.
 * An I2PTunnel tracks one or more I2PTunnelTasks and one or more I2PSessions.
 * Usually one of each.
 *
 * These objects are bundled together under a TunnelControllerGroup where the
 * entire group is stored / loaded from a single config file.
 *
 * This is the class used by several plugins to create tunnels, so
 * take care to maintain the public methods as a stable API.
 */
public class TunnelController implements Logging {
    private final Log _log;
    private Properties _config;
    private final I2PTunnel _tunnel;
    private final List<String> _messages;
    private List<I2PSession> _sessions;
    private volatile TunnelState _state;

    /** @since 0.9.19 */
    private enum TunnelState {
        START_ON_LOAD,
        STARTING,
        RUNNING,
        STOPPING,
        STOPPED,
        DESTROYING,
        DESTROYED,
    }
    
    public static final String KEY_BACKUP_DIR = "i2ptunnel-keyBackup";

    /** all of these @since 0.9.14 */
    public static final String PROP_DESCR = "description";
    public static final String PROP_DEST = "targetDestination";
    public static final String PROP_I2CP_HOST = "i2cpHost";
    public static final String PROP_I2CP_PORT = "i2cpPort";
    public static final String PROP_INTFC = "interface";
    public static final String PROP_FILE = "privKeyFile";
    public static final String PROP_LISTEN_PORT = "listenPort";
    public static final String PROP_NAME = "name";
    public static final String PROP_PROXIES = "proxyList";
    public static final String PROP_SHARED = "sharedClient";
    public static final String PROP_SPOOFED_HOST = "spoofedHost";
    public static final String PROP_START = "startOnLoad";
    public static final String PROP_TARGET_HOST = "targetHost";
    public static final String PROP_TARGET_PORT = "targetPort";
    public static final String PROP_TYPE = "type";

    /** @since 0.9.14 */
    public static final String PFX_OPTION = "option.";

    private static final String OPT_PERSISTENT = PFX_OPTION + "persistentClientKey";
    public static final String OPT_BUNDLE_REPLY = PFX_OPTION + "shouldBundleReplyInfo";
    private static final String OPT_TAGS_SEND = PFX_OPTION + "crypto.tagsToSend";
    private static final String OPT_LOW_TAGS = PFX_OPTION + "crypto.lowTagThreshold";
    private static final String OPT_SIG_TYPE = PFX_OPTION + I2PClient.PROP_SIGTYPE;
    /** @since 0.9.30 */
    private static final String OPT_ALT_PKF = PFX_OPTION + I2PTunnelServer.PROP_ALT_PKF;

    /** all of these @since 0.9.14 */
    public static final String TYPE_CONNECT = "connectclient";
    public static final String TYPE_HTTP_BIDIR_SERVER = "httpbidirserver";
    public static final String TYPE_HTTP_CLIENT = "httpclient";
    public static final String TYPE_HTTP_SERVER = "httpserver";
    public static final String TYPE_IRC_CLIENT = "ircclient";
    public static final String TYPE_IRC_SERVER = "ircserver";
    public static final String TYPE_SOCKS = "sockstunnel";
    public static final String TYPE_SOCKS_IRC = "socksirctunnel";
    public static final String TYPE_STD_CLIENT = "client";
    public static final String TYPE_STD_SERVER = "server";
    /** Client in the UI and I2P side but a server on the localhost side */
    public static final String TYPE_STREAMR_CLIENT = "streamrclient";
    /** Server in the UI and I2P side but a client on the localhost side */
    public static final String TYPE_STREAMR_SERVER = "streamrserver";

    /**
     *  This is guaranteed to be available.
     *  @since 0.9.17
     */
    public static final SigType PREFERRED_SIGTYPE;
    static {
        if (SystemVersion.isGNU() || SystemVersion.isAndroid()) {
            if (SigType.ECDSA_SHA256_P256.isAvailable())
                PREFERRED_SIGTYPE = SigType.ECDSA_SHA256_P256;
            else
                PREFERRED_SIGTYPE = SigType.DSA_SHA1;
        } else {
            PREFERRED_SIGTYPE = SigType.EdDSA_SHA512_Ed25519;
        }
    }

    /**
     * Create a new controller for a tunnel out of the specific config options.
     * The config may contain a large number of options - only ones that begin in
     * the prefix should be used (and, in turn, that prefix should be stripped off
     * before being interpreted by this controller)
     * 
     * Defaults in config properties are not recommended, they may or may not be honored.
     * 
     * @param config original key=value mapping non-null
     * @param prefix beginning of key values that are relevant to this tunnel
     */
    public TunnelController(Properties config, String prefix) {
        this(config, prefix, true);
    }

    /**
     * Defaults in config properties are not recommended, they may or may not be honored.
     * 
     * @param config original key=value mapping non-null
     * @param prefix beginning of key values that are relevant to this tunnel
     * @param createKey for servers, whether we want to create a brand new destination
     *                  with private keys at the location specified or not (does not
     *                  overwrite existing ones)
     */
    public TunnelController(Properties config, String prefix, boolean createKey) {
        _tunnel = new I2PTunnel();
        _log = I2PAppContext.getGlobalContext().logManager().getLog(TunnelController.class);
        setConfig(config, prefix);
        _messages = new ArrayList<String>(4);
        boolean keyOK = true;
        if (createKey && (!isClient() || getPersistentClientKey())) {
            keyOK = createPrivateKey();
            if (keyOK && !isClient() && !getType().equals(TYPE_STREAMR_SERVER)) {
                // check rv?
                createAltPrivateKey();
            }
        }
        _state = keyOK && getStartOnLoad() ? TunnelState.START_ON_LOAD : TunnelState.STOPPED;
    }
    
    /**
     * @return success
     */
    private boolean createPrivateKey() {
        I2PClient client = I2PClientFactory.createClient();
        File keyFile = getPrivateKeyFile();
        if (keyFile == null) {
            log("No filename specified for the private key");
            return false;
        }
        
        if (keyFile.exists()) {
            //log("Not overwriting existing private keys in " + keyFile.getAbsolutePath());
            return true;
        } else {
            File parent = keyFile.getParentFile();
            if ( (parent != null) && (!parent.exists()) )
                parent.mkdirs();
        }
        FileOutputStream fos = null;
        try {
            fos = new SecureFileOutputStream(keyFile);
            SigType stype = PREFERRED_SIGTYPE;
            String st = _config.getProperty(OPT_SIG_TYPE);
            if (st != null) {
                SigType type = SigType.parseSigType(st);
                if (type != null && type.isAvailable())
                    stype = type;
                else
                    log("Unsupported sig type " + st + ", reverting to " + stype);
            }
            Destination dest = client.createDestination(fos, stype);
            String destStr = dest.toBase64();
            log("Private key created and saved in " + keyFile.getAbsolutePath());
            log("You should backup this file in a secure place.");
            log("New alternate destination: " + destStr);
            String b32 = dest.toBase32();
            log("Base32: " + b32);
            File backupDir = new SecureFile(I2PAppContext.getGlobalContext().getConfigDir(), KEY_BACKUP_DIR);
            if (backupDir.isDirectory() || backupDir.mkdir()) {
                String name = b32 + '-' + I2PAppContext.getGlobalContext().clock().now() + ".dat";
                File backup = new File(backupDir, name);
                if (FileUtil.copy(keyFile, backup, false, true)) {
                    SecureFileOutputStream.setPerms(backup);
                    log("Private key backup saved to " + backup.getAbsolutePath());
                }
            }
        } catch (I2PException ie) {
            if (_log.shouldLog(Log.ERROR))
                _log.error("Error creating new destination", ie);
            log("Error creating new destination: " + ie.getMessage());
            return false;
        } catch (IOException ioe) {
            if (_log.shouldLog(Log.ERROR))
                _log.error("Error creating writing the destination to " + keyFile.getAbsolutePath(), ioe);
            log("Error writing the keys to " + keyFile.getAbsolutePath());
            return false;
        } finally {
            if (fos != null) try { fos.close(); } catch (IOException ioe) {}
        }
        return true;
    }
    
    /**
     * Creates alternate Destination with the same encryption keys as the primary Destination,
     * but a different signing key.
     *
     * Must have already called createPrivateKey() successfully.
     * Does nothing unless option OPT_ALT_PKF is set with the privkey file name.
     * Does nothing if the file already exists.
     *
     * @return success
     * @since 0.9.30
     */
    private boolean createAltPrivateKey() {
        if (PREFERRED_SIGTYPE == SigType.DSA_SHA1)
            return false;
        File keyFile = getPrivateKeyFile();
        if (keyFile == null)
            return false;
        if (!keyFile.exists())
            return false;
        File altFile = getAlternatePrivateKeyFile();
        if (altFile == null)
            return false;
        if (altFile.equals(keyFile))
            return false;
        if (altFile.exists())
            return true;
        PrivateKeyFile pkf = new PrivateKeyFile(keyFile);
        FileOutputStream out = null;
        try {
            Destination dest = pkf.getDestination();
            if (dest == null)
                return false;
            if (dest.getSigType() != SigType.DSA_SHA1)
                return false;
            PublicKey pub = dest.getPublicKey();
            PrivateKey priv = pkf.getPrivKey();
            SimpleDataStructure[] signingKeys = KeyGenerator.getInstance().generateSigningKeys(PREFERRED_SIGTYPE);
            SigningPublicKey signingPubKey = (SigningPublicKey) signingKeys[0];
            SigningPrivateKey signingPrivKey = (SigningPrivateKey) signingKeys[1];
            KeyCertificate cert = new KeyCertificate(signingPubKey);
            Destination d = new Destination();
            d.setPublicKey(pub);
            d.setSigningPublicKey(signingPubKey);
            d.setCertificate(cert);
            int len = signingPubKey.length();
            if (len < 128) {
                byte[] pad = new byte[128 - len];
                RandomSource.getInstance().nextBytes(pad);
                d.setPadding(pad);
            } else if (len > 128) {
                // copy of excess data handled in KeyCertificate constructor
            }
        
            out = new SecureFileOutputStream(altFile);
            d.writeBytes(out);
            priv.writeBytes(out);
            signingPrivKey.writeBytes(out);
            try { out.close(); } catch (IOException ioe) {}

            String destStr = d.toBase64();
            log("Alternate private key created and saved in " + altFile.getAbsolutePath());
            log("You should backup this file in a secure place.");
            log("New destination: " + destStr);
            String b32 = d.toBase32();
            log("Base32: " + b32);
            File backupDir = new SecureFile(I2PAppContext.getGlobalContext().getConfigDir(), KEY_BACKUP_DIR);
            if (backupDir.isDirectory() || backupDir.mkdir()) {
                String name = b32 + '-' + I2PAppContext.getGlobalContext().clock().now() + ".dat";
                File backup = new File(backupDir, name);
                if (FileUtil.copy(altFile, backup, false, true)) {
                    SecureFileOutputStream.setPerms(backup);
                    log("Alternate private key backup saved to " + backup.getAbsolutePath());
                }
            }
            return true;
        } catch (GeneralSecurityException e) {
            log("Error creating keys " + e);
            return false;
        } catch (I2PSessionException e) {
            log("Error creating keys " + e);
            return false;
        } catch (I2PException e) {
            log("Error creating keys " + e);
            return false;
        } catch (IOException e) {
            log("Error creating keys " + e);
            return false;
        } catch (RuntimeException e) {
            log("Error creating keys " + e);
            return false;
        } finally {
            if (out != null) try { out.close(); } catch (IOException ioe) {}
        }
    }
    
    public void startTunnelBackground() {
        synchronized (this) {
            if (_state != TunnelState.STOPPED && _state != TunnelState.START_ON_LOAD)
                return;
        }
        new I2PAppThread(new Runnable() { public void run() { startTunnel(); } }, "Tunnel Starter " + getName()).start();
    }
    
    /**
     * Start up the tunnel (if it isn't already running)
     *
     */
    public void startTunnel() {
        synchronized (this) {
            if (_state != TunnelState.STOPPED && _state != TunnelState.START_ON_LOAD) {
                if (_state == TunnelState.RUNNING) {
                    if (_log.shouldLog(Log.INFO))
                        _log.info("Already running");
                    log("Tunnel " + getName() + " is already running");
                }
                return;
            }
            changeState(TunnelState.STARTING);
        }
        try {
            doStartTunnel();
        } catch (RuntimeException e) {
            _log.error("Error starting the tunnel " + getName(), e);
            log("Error starting the tunnel " + getName() + ": " + e.getMessage());
            // if we don't acquire() then the release() in stopTunnel() won't work
            acquire();
            stopTunnel();
        }
    }

    /**
     *  @throws IllegalArgumentException via methods in I2PTunnel
     */
    private void doStartTunnel() {
        synchronized (this) {
            if (_state != TunnelState.STARTING)
                return;
        }

        String type = getType(); 
        if ( (type == null) || (type.length() <= 0) ) {
            changeState(TunnelState.STOPPED);
            if (_log.shouldLog(Log.ERROR))
                _log.error("Cannot start the tunnel - no type specified");
            return;
        }
        // Config options may have changed since instantiation, so do this again.
        // Or should we take it out of the constructor completely?
        if (!isClient() || getPersistentClientKey()) {
            boolean ok = createPrivateKey();
            if (!ok) {
                changeState(TunnelState.STOPPED);
                log("Failed to start tunnel " + getName() + " as the private key file could not be created");
                return;
            }
            if (!isClient() && !getType().equals(TYPE_STREAMR_SERVER)) {
                // check rv?
                createAltPrivateKey();
            }
        }
        setI2CPOptions();
        setSessionOptions();
        if (TYPE_HTTP_CLIENT.equals(type)) {
            startHttpClient();
        } else if(TYPE_IRC_CLIENT.equals(type)) {
            startIrcClient();
        } else if(TYPE_SOCKS.equals(type)) {
            startSocksClient();
        } else if(TYPE_SOCKS_IRC.equals(type)) {
            startSocksIRCClient();
        } else if(TYPE_CONNECT.equals(type)) {
            startConnectClient();
        } else if (TYPE_STD_CLIENT.equals(type)) {
            startClient();
        } else if (TYPE_STREAMR_CLIENT.equals(type)) {
            startStreamrClient();
        } else if (TYPE_STD_SERVER.equals(type)) {
            startServer();
        } else if (TYPE_HTTP_SERVER.equals(type)) {
            startHttpServer();
        } else if (TYPE_HTTP_BIDIR_SERVER.equals(type)) {
            startHttpBidirServer();
        } else if (TYPE_IRC_SERVER.equals(type)) {
            startIrcServer();
        } else if (TYPE_STREAMR_SERVER.equals(type)) {
            startStreamrServer();
        } else {
            changeState(TunnelState.STOPPED);
            if (_log.shouldLog(Log.ERROR))
                _log.error("Cannot start tunnel - unknown type [" + type + "]");
            return;
        }
        acquire();
        changeState(TunnelState.RUNNING);
    }
    
    private void startHttpClient() {
        setListenOn();
        String listenPort = getListenPort();
        String proxyList = getProxyList();
        String sharedClient = getSharedClient();
        if (proxyList == null)
            _tunnel.runHttpClient(new String[] { listenPort, sharedClient }, this);
        else
            _tunnel.runHttpClient(new String[] { listenPort, sharedClient, proxyList }, this);
    }
    
    private void startConnectClient() {
        setListenOn();
        String listenPort = getListenPort();
        String proxyList = getProxyList();
        String sharedClient = getSharedClient();
        if (proxyList == null)
            _tunnel.runConnectClient(new String[] { listenPort, sharedClient }, this);
        else
            _tunnel.runConnectClient(new String[] { listenPort, sharedClient, proxyList }, this);
    }
    
    private void startIrcClient() {
        setListenOn();
        String listenPort = getListenPort();
        String dest = getTargetDestination();
        String sharedClient = getSharedClient();
        if (getPersistentClientKey()) {
            String privKeyFile = getPrivKeyFile(); 
            _tunnel.runIrcClient(new String[] { listenPort, dest, sharedClient, privKeyFile }, this);
        } else {
            _tunnel.runIrcClient(new String[] { listenPort, dest, sharedClient }, this);
        }
    }
    
    private void startSocksClient() {
        setListenOn();
        String listenPort = getListenPort();
        String sharedClient = getSharedClient();
        String proxyList = getProxyList();
        if (proxyList != null) {
            // set the outproxy property the socks tunnel wants
            Properties props = _tunnel.getClientOptions();
            if (!props.containsKey(I2PSOCKSTunnel.PROP_PROXY_DEFAULT))
                props.setProperty(I2PSOCKSTunnel.PROP_PROXY_DEFAULT, proxyList);
        }
        if (getPersistentClientKey()) {
            String privKeyFile = getPrivKeyFile(); 
            _tunnel.runSOCKSTunnel(new String[] { listenPort, "false", privKeyFile }, this);
        } else {
            _tunnel.runSOCKSTunnel(new String[] { listenPort, sharedClient }, this);
        }
    }
    
    /** @since 0.7.12 */
    private void startSocksIRCClient() {
        setListenOn();
        String listenPort = getListenPort();
        String sharedClient = getSharedClient();
        String proxyList = getProxyList();
        if (proxyList != null) {
            // set the outproxy property the socks tunnel wants
            Properties props = _tunnel.getClientOptions();
            if (!props.containsKey(I2PSOCKSTunnel.PROP_PROXY_DEFAULT))
                props.setProperty(I2PSOCKSTunnel.PROP_PROXY_DEFAULT, proxyList);
        }
        if (getPersistentClientKey()) {
            String privKeyFile = getPrivKeyFile(); 
            _tunnel.runSOCKSIRCTunnel(new String[] { listenPort, "false", privKeyFile }, this);
        } else {
            _tunnel.runSOCKSIRCTunnel(new String[] { listenPort, sharedClient }, this);
        }
    }
    
    /*
     *  Streamr client is a UDP server, use the listenPort field for targetPort
     */
    private void startStreamrClient() {
        String targetHost = getTargetHost();
        String targetPort = getListenPort();
        String dest = getTargetDestination();
        _tunnel.runStreamrClient(new String[] { targetHost, targetPort, dest }, this);
    }
    
    /**
     *  Streamr server is a UDP client, use the targetPort field for listenPort
     */
    private void startStreamrServer() {
        String listenOn = getListenOnInterface();
        if ( (listenOn != null) && (listenOn.length() > 0) ) {
            _tunnel.runListenOn(new String[] { listenOn }, this);
        }
        String listenPort = getTargetPort();
        String privKeyFile = getPrivKeyFile(); 
        _tunnel.runStreamrServer(new String[] { listenPort, privKeyFile }, this);
    }
    
    /** 
     * Note the fact that we are using some sessions, so that they dont get
     * closed by some other tunnels
     */
    private void acquire() {
        List<I2PSession> sessions = _tunnel.getSessions();
        if (!sessions.isEmpty()) {
            for (int i = 0; i < sessions.size(); i++) {
                I2PSession session = sessions.get(i);
                if (_log.shouldLog(Log.INFO))
                    _log.info("Acquiring session " + session);
                TunnelControllerGroup group = TunnelControllerGroup.getInstance();
                if (group != null)
                    group.acquire(this, session);
            }
            _sessions = sessions;
        } else {
            if (_log.shouldLog(Log.WARN))
                _log.warn("No sessions to acquire? for " + getName());
        }
    }
    
    /** 
     * Note the fact that we are no longer using some sessions, and if
     * no other tunnels are using them, close them.
     */
    private void release(Collection<I2PSession> sessions) {
        if (!sessions.isEmpty()) {
            for (I2PSession s : sessions) {
                if (_log.shouldLog(Log.INFO))
                    _log.info("Releasing session " + s);
                TunnelControllerGroup group = TunnelControllerGroup.getInstance();
                if (group != null)
                    group.release(this, s);
            }
            // _sessions.clear() ????
        } else {
            if (_log.shouldLog(Log.WARN))
                _log.warn("No sessions to release? for " + getName());
        }
    }

    /** 
     *  Get all the sessions we may be using.
     *
     *  @return a copy, non-null
     *  @since 0.9.15
     */
    private Collection<I2PSession> getAllSessions() {
        // We use _sessions AND the tunnel sessions as
        // _sessions will be null for delay-open tunnels - see acquire().
        // We want the current sessions.
        Set<I2PSession> sessions = new HashSet<I2PSession>(_tunnel.getSessions());
        if (_sessions != null)
            sessions.addAll(_sessions);
        return sessions;
    }
    
    private void startClient() {
        setListenOn();
        String listenPort = getListenPort(); 
        String dest = getTargetDestination();
        String sharedClient = getSharedClient();
        if (getPersistentClientKey()) {
            String privKeyFile = getPrivKeyFile(); 
            _tunnel.runClient(new String[] { listenPort, dest, sharedClient, privKeyFile }, this);
        } else {
            _tunnel.runClient(new String[] { listenPort, dest, sharedClient }, this);
        }
    }

    private void startServer() {
        String targetHost = getTargetHost(); 
        String targetPort = getTargetPort(); 
        String privKeyFile = getPrivKeyFile(); 
        _tunnel.runServer(new String[] { targetHost, targetPort, privKeyFile }, this);
    }
    
    private void startHttpServer() {
        String targetHost = getTargetHost(); 
        String targetPort = getTargetPort(); 
        String spoofedHost = getSpoofedHost(); 
        String privKeyFile = getPrivKeyFile(); 
        _tunnel.runHttpServer(new String[] { targetHost, targetPort, spoofedHost, privKeyFile }, this);
    }
    
    private void startHttpBidirServer() {
        setListenOn();
        String targetHost = getTargetHost();
        String targetPort = getTargetPort();
        String listenPort = getListenPort();
        String spoofedHost = getSpoofedHost();
        String privKeyFile = getPrivKeyFile();
        _tunnel.runHttpBidirServer(new String[] { targetHost, targetPort, listenPort, spoofedHost, privKeyFile }, this);
    }

    private void startIrcServer() {
        String targetHost = getTargetHost(); 
        String targetPort = getTargetPort(); 
        String privKeyFile = getPrivKeyFile(); 
        _tunnel.runIrcServer(new String[] { targetHost, targetPort, privKeyFile }, this);
    }
    
    private void setListenOn() {
        String listenOn = getListenOnInterface();
        if ( (listenOn != null) && (listenOn.length() > 0) ) {
            _tunnel.runListenOn(new String[] { listenOn }, this);
        }
    }
    
    /**
     *  These are the ones stored with a prefix of "option."
     *  Defaults in config properties are not honored.
     *
     *  @return keys with the "option." prefix stripped, non-null
     *  @since 0.9.1 Much better than getClientOptions()
     */
    public Properties getClientOptionProps() {
        Properties opts = new Properties();
        for (Map.Entry<Object, Object> e : _config.entrySet()) {
            String key = (String) e.getKey();
            if (key.startsWith(PFX_OPTION)) {
                key = key.substring(PFX_OPTION.length());
                String val = (String) e.getValue();
                opts.setProperty(key, val);
            }
        }
        return opts;
    }

    private void setSessionOptions() {
        Properties opts = getClientOptionProps();
        // targetDestination does NOT start with "option.", but we still want
        // to allow a change on the fly, so we pass it through this way,
        // as a "spoofed" option. Since 0.9.9.
        String target = getTargetDestination();
        if (target != null)
            opts.setProperty(PROP_DEST, target);
        // Ditto outproxy list. Since 0.9.12.
        String proxies = getProxyList();
        if (proxies != null)
            opts.setProperty(PROP_PROXIES, proxies);
        // Ditto spoof host. Since 0.9.15.
        String spoofhost = getSpoofedHost();
        if (spoofhost != null)
            opts.setProperty(PROP_SPOOFED_HOST, spoofhost);
        // Ditto target host/port. Since 0.9.15.
        String targethost = getTargetHost();
        if (targethost != null)
            opts.setProperty(PROP_TARGET_HOST, targethost);
        String targetport = getTargetPort();
        if (targetport != null)
            opts.setProperty(PROP_TARGET_PORT, targetport);
        _tunnel.setClientOptions(opts);
    }
    
    private void setI2CPOptions() {
        String host = getI2CPHost();
        if ( (host != null) && (host.length() > 0) ) 
            _tunnel.host = host;
        // woohah, special casing for people with ipv6/etc
        if ("localhost".equals(_tunnel.host)) 
            _tunnel.host = "127.0.0.1";
        String port = getI2CPPort();
        if ( (port != null) && (port.length() > 0) ) {
            try {
                int portNum = Integer.parseInt(port);
                _tunnel.port = String.valueOf(portNum);
            } catch (NumberFormatException nfe) {
                _tunnel.port = "7654";
            }
        } else {
            _tunnel.port = "7654";
        }
    }

    /**
     *  May be restarted with restartTunnel() or startTunnel() later.
     *  This may not release all resources. In particular, the I2PSocketManager remains
     *  and it may have timer threads that continue running.
     */
    public void stopTunnel() {
        synchronized (this) {
            if (_state != TunnelState.STARTING && _state != TunnelState.RUNNING)
                return;
            changeState(TunnelState.STOPPING);
        }
        // I2PTunnel removes the session in close(),
        // so save the sessions to pass to release() and TCG
        Collection<I2PSession> sessions = getAllSessions();
        _tunnel.runClose(new String[] { "forced", "all" }, this);
        release(sessions);
        changeState(TunnelState.STOPPED);
    }

    /**
     *  May NOT be restarted with restartTunnel() or startTunnel() later.
     *  This should release all resources.
     *
     *  @since 0.9.17
     */
    public void destroyTunnel() {
        synchronized (this) {
            if (_state != TunnelState.RUNNING)
                return;
            changeState(TunnelState.DESTROYING);
        }
        // I2PTunnel removes the session in close(),
        // so save the sessions to pass to release() and TCG
        Collection<I2PSession> sessions = getAllSessions();
        _tunnel.runClose(new String[] { "destroy", "all" }, this);
        release(sessions);
        changeState(TunnelState.DESTROYED);
    }
    
    public void restartTunnel() {
        stopTunnel();
        startTunnel();
    }
    
    /**
     *  As of 0.9.1, updates the options on an existing session
     */
    public void setConfig(Properties config, String prefix) {
        Properties props = new Properties();
        for (Map.Entry<Object, Object> e : config.entrySet()) {
            String key = (String) e.getKey();
            if (key.startsWith(prefix)) {
                key = key.substring(prefix.length());
                String val = (String) e.getValue();
                props.setProperty(key, val);
            }
        }
        Properties oldConfig = _config;
        _config = props;

        // Set up some per-type defaults
        // This really isn't the best spot to do this but for servers in particular,
        // it's hard to override settings in the subclass since the session connect
        // is done in the I2PTunnelServer constructor.
        String type = getType();
        if (type != null) {
            if (type.equals(TYPE_HTTP_SERVER) || type.equals(TYPE_STREAMR_SERVER)) {
                if (!_config.containsKey(OPT_BUNDLE_REPLY))
                    _config.setProperty(OPT_BUNDLE_REPLY, "false");
            } else if (!isClient(type)) {
                // override UI that sets it to false
                _config.setProperty(OPT_BUNDLE_REPLY, "true");
            }
            if (type.contains("irc") || type.equals(TYPE_STREAMR_CLIENT)) {
                // maybe a bad idea for ircclient if DCC is enabled
                if (!_config.containsKey(OPT_TAGS_SEND))
                    _config.setProperty(OPT_TAGS_SEND, "20");
                if (!_config.containsKey(OPT_LOW_TAGS))
                    _config.setProperty(OPT_LOW_TAGS, "14");
            }
            // same default logic as in EditBean.getSigType()
            if (!isClient(type) ||
                type.equals(TYPE_IRC_CLIENT) || type.equals(TYPE_STD_CLIENT) ||
                type.equals(TYPE_SOCKS) ||
                type.equals(TYPE_SOCKS_IRC) || type.equals(TYPE_STREAMR_CLIENT) ||
                (type.equals(TYPE_HTTP_CLIENT) && Boolean.valueOf(getSharedClient()))) {
                if (!_config.containsKey(OPT_SIG_TYPE))
                    _config.setProperty(OPT_SIG_TYPE, PREFERRED_SIGTYPE.name());
            }
        }

        // tell i2ptunnel, who will tell the TunnelTask, who will tell the SocketManager
        setSessionOptions();

        synchronized (this) {
            if (_state != TunnelState.RUNNING) {
                if (_log.shouldLog(Log.DEBUG)) {
                    _log.debug("Not running, not updating sessions");
                }
                return;
            }
        }

        if (oldConfig != null) {
            if (configChanged(_config, oldConfig, PROP_FILE) ||
                configChanged(_config, oldConfig, OPT_ALT_PKF) ||
                configChanged(_config, oldConfig, OPT_SIG_TYPE)) {
                log("Tunnel must be stopped and restarted for private key file changes to take effect");
            }
        }

        // Running, so check sessions
        Collection<I2PSession> sessions = getAllSessions();
        if (sessions.isEmpty()) {
             if (_log.shouldLog(Log.DEBUG))
                 _log.debug("Running but no sessions to update");
        }
        for (I2PSession s : sessions) {
            // tell the router via the session
            if (!s.isClosed()) {
                if (_log.shouldLog(Log.DEBUG))
                    _log.debug("Session is open, updating: " + s);
                s.updateOptions(_tunnel.getClientOptions());
            } else {
                if (_log.shouldLog(Log.DEBUG))
                    _log.debug("Session is closed, not updating: " + s);
            }
        }
    }

    /**
     *  Is property p different in p1 and p2?
     *  @since 0.9.30
     */
    private static boolean configChanged(Properties p1, Properties p2, String p) {
        String s1 = p1.getProperty(p);
        String s2 = p2.getProperty(p);
        return (s1 != null && !s1.equals(s2)) ||
               (s1 == null && s2 != null);
    }

    /**
     *  @return a copy
     */
    public Properties getConfig(String prefix) { 
        Properties rv = new Properties();
        for (Map.Entry<Object, Object> e : _config.entrySet()) {
            String key = (String) e.getKey();
            String val = (String) e.getValue();
            rv.setProperty(prefix + key, val);
        }
        return rv;
    }

    public String getType() { return _config.getProperty(PROP_TYPE); }
    public String getName() { return _config.getProperty(PROP_NAME); }
    public String getDescription() { return _config.getProperty(PROP_DESCR); }
    public String getI2CPHost() { return _config.getProperty(PROP_I2CP_HOST); }
    public String getI2CPPort() { return _config.getProperty(PROP_I2CP_PORT); }

    /**
     *  Is it a client or server in the UI and I2P side?
     *  Note that a streamr client is a UI and I2P client but a server on the localhost side.
     *  Note that a streamr server is a UI and I2P server but a client on the localhost side.
     *
     *  @since 0.9.17
     */
    public boolean isClient() {
        return isClient(getType());
    }

    /**
     *  Is it a client or server in the UI and I2P side?
     *  Note that a streamr client is a UI and I2P client but a server on the localhost side.
     *  Note that a streamr server is a UI and I2P server but a client on the localhost side.
     *
     *  @since 0.9.17 moved from IndexBean
     */
    public static boolean isClient(String type) {
        return TYPE_STD_CLIENT.equals(type) || 
               TYPE_HTTP_CLIENT.equals(type) ||
               TYPE_SOCKS.equals(type) ||
               TYPE_SOCKS_IRC.equals(type) ||
               TYPE_CONNECT.equals(type) ||
               TYPE_STREAMR_CLIENT.equals(type) ||
               TYPE_IRC_CLIENT.equals(type);
    }

    /**
     *  These are the ones with a prefix of "option."
     *
     *  @return one big string of "key=val key=val ..."
     *  @deprecated why would you want this? Use getClientOptionProps() instead
     */
    @Deprecated
    public String getClientOptions() {
        StringBuilder opts = new StringBuilder(64);
        for (Map.Entry<Object, Object> e : _config.entrySet()) {
            String key = (String) e.getKey();
            if (key.startsWith(PFX_OPTION)) {
                key = key.substring(PFX_OPTION.length());
                String val = (String) e.getValue();
                if (opts.length() > 0) opts.append(' ');
                opts.append(key).append('=').append(val);
            }
        }
        return opts.toString();
    }

    public String getListenOnInterface() { return _config.getProperty(PROP_INTFC); }
    public String getTargetHost() { return _config.getProperty(PROP_TARGET_HOST); }
    public String getTargetPort() { return _config.getProperty(PROP_TARGET_PORT); }
    public String getSpoofedHost() { return _config.getProperty(PROP_SPOOFED_HOST); }

    /**
     *  Probably not absolute. May be null. getPrivateKeyFile() recommended.
     */
    public String getPrivKeyFile() { return _config.getProperty(PROP_FILE); }

    public String getListenPort() { return _config.getProperty(PROP_LISTEN_PORT); }
    public String getTargetDestination() { return _config.getProperty(PROP_DEST); }
    public String getProxyList() { return _config.getProperty(PROP_PROXIES); }
    /** default true */
    public String getSharedClient() { return _config.getProperty(PROP_SHARED, "true"); }
    /** default true */
    public boolean getStartOnLoad() { return Boolean.parseBoolean(_config.getProperty(PROP_START, "true")); }
    public boolean getPersistentClientKey() { return Boolean.parseBoolean(_config.getProperty(OPT_PERSISTENT)); }

    /**
     *  Does not necessarily exist.
     *  @return absolute path or null if unset
     *  @since 0.9.17
     */
    public File getPrivateKeyFile() {
        return filenameToFile(getPrivKeyFile());
    }

    /**
     *  Does not necessarily exist.
     *  @return absolute path or null if unset
     *  @since 0.9.30
     */
    public File getAlternatePrivateKeyFile() {
        return filenameToFile(_config.getProperty(OPT_ALT_PKF));
    }

    /**
     *  Does not necessarily exist.
     *  @param f relative or absolute path, may be null
     *  @return absolute path or null
     *  @since 0.9.30
     */
    static File filenameToFile(String f) {
        if (f == null)
            return null;
        f = f.trim();
        if (f.length() == 0)
            return null;
        File rv = new File(f);
        if (!rv.isAbsolute())
            rv = new File(I2PAppContext.getGlobalContext().getConfigDir(), f);
        return rv;
    }

    /**
     *  Returns null if not running.
     *  @return Base64 or null
     */
    public String getMyDestination() {
        Destination dest = getDestination();
        if (dest != null)
            return dest.toBase64();
        return null;
    }
    
    /**
     *  Returns null if not running.
     *  @return "{52 chars}.b32.i2p" or null
     */
    public String getMyDestHashBase32() {
        Destination dest = getDestination();
        if (dest != null)
            return dest.toBase32();
        return null;
    }
    
    /**
     *  Returns null if not running.
     *  @return Destination or null
     *  @since 0.9.17
     */
    public Destination getDestination() {
        if (_tunnel != null) {
            List<I2PSession> sessions = _tunnel.getSessions();
            for (int i = 0; i < sessions.size(); i++) {
                I2PSession session = sessions.get(i);
                Destination dest = session.getMyDestination();
                if (dest != null)
                    return dest;
            }
        }
        return null;
    }
    
    public boolean getIsRunning() { return _state == TunnelState.RUNNING; }
    public boolean getIsStarting() { return _state == TunnelState.START_ON_LOAD || _state == TunnelState.STARTING; }

    /** if running but no open sessions, we are in standby */
    public boolean getIsStandby() {
        synchronized (this) {
            if (_state != TunnelState.RUNNING)
                return false;
        }

        for (I2PSession sess : _tunnel.getSessions()) {
            if (!sess.isClosed())
                return false;
        }
        return true;
    }

    /** @since 0.9.19 */
    private synchronized void changeState(TunnelState state) {
        _state = state;
    }
    
    /**
     *  A text description of the tunnel.
     *  @deprecated unused
     */
    @Deprecated
    public void getSummary(StringBuilder buf) {
        String type = getType();
        buf.append(type);
      /****
        if ("httpclient".equals(type))
            getHttpClientSummary(buf);
        else if ("client".equals(type))
            getClientSummary(buf);
        else if ("server".equals(type))
            getServerSummary(buf);
        else if ("httpserver".equals(type))
            getHttpServerSummary(buf);
        else
            buf.append("Unknown type ").append(type);
       ****/
    }
    
  /****
    private void getHttpClientSummary(StringBuilder buf) {
        String description = getDescription();
        if ( (description != null) && (description.trim().length() > 0) )
            buf.append("<i>").append(description).append("</i><br />\n");
        buf.append("HTTP proxy listening on port ").append(getListenPort());
        String listenOn = getListenOnInterface();
        if ("0.0.0.0".equals(listenOn)) 
            buf.append(" (reachable by any machine)");
        else if ("127.0.0.1".equals(listenOn))
            buf.append(" (reachable locally only)");
        else
            buf.append(" (reachable at the ").append(listenOn).append(" interface)");
        buf.append("<br />\n");
        String proxies = getProxyList();
        if ( (proxies == null) || (proxies.trim().length() <= 0) )
            buf.append("Outproxy: default [squid.i2p]<br />\n");
        else
            buf.append("Outproxy: ").append(proxies).append("<br />\n");
        getOptionSummary(buf);
    }
    
    private void getClientSummary(StringBuilder buf) {
        String description = getDescription();
        if ( (description != null) && (description.trim().length() > 0) )
            buf.append("<i>").append(description).append("</i><br />\n");
        buf.append("Client tunnel listening on port ").append(getListenPort());
        buf.append(" pointing at ").append(getTargetDestination());
        String listenOn = getListenOnInterface();
        if ("0.0.0.0".equals(listenOn)) 
            buf.append(" (reachable by any machine)");
        else if ("127.0.0.1".equals(listenOn))
            buf.append(" (reachable locally only)");
        else
            buf.append(" (reachable at the ").append(listenOn).append(" interface)");
        buf.append("<br />\n");
        getOptionSummary(buf);
    }
    
    private void getServerSummary(StringBuilder buf) {
        String description = getDescription();
        if ( (description != null) && (description.trim().length() > 0) )
            buf.append("<i>").append(description).append("</i><br />\n");
        buf.append("Server tunnel pointing at port ").append(getTargetPort());
        buf.append(" on ").append(getTargetHost());
        buf.append("<br />\n");
        buf.append("Private destination loaded from ").append(getPrivKeyFile()).append("<br />\n");
        getOptionSummary(buf);
    }
    
    private void getHttpServerSummary(StringBuilder buf) {
        String description = getDescription();
        if ( (description != null) && (description.trim().length() > 0) )
            buf.append("<i>").append(description).append("</i><br />\n");
        buf.append("Server tunnel pointing at port ").append(getTargetPort());
        buf.append(" on ").append(getTargetHost());
        buf.append(" for the site ").append(getSpoofedHost());
        buf.append("<br />\n");
        buf.append("Private destination loaded from ").append(getPrivKeyFile()).append("<br />\n");
        getOptionSummary(buf);
    }
    
    private void getOptionSummary(StringBuilder buf) {
        String opts = getClientOptions();
        if ( (opts != null) && (opts.length() > 0) )
            buf.append("Network options: ").append(opts).append("<br />\n");
        if (_running) {
            List<I2PSession> sessions = _tunnel.getSessions();
            for (int i = 0; i < sessions.size(); i++) {
                I2PSession session = sessions.get(i);
                Destination dest = session.getMyDestination();
                if (dest != null) {
                    buf.append("Destination hash: ").append(dest.calculateHash().toBase64()).append("<br />\n");
                    if ( ("server".equals(getType())) || ("httpserver".equals(getType())) ) {
                        buf.append("Full destination: ");
                        buf.append("<input type=\"text\" size=\"10\" onclick=\"this.select();\" ");
                        buf.append("value=\"").append(dest.toBase64()).append("\" />\n");
                        long val = new Random().nextLong();
                        if (val < 0) val = 0 - val;
                        buf.append("<br />You can <a href=\"http://temp").append(val);
                        buf.append(".i2p/?i2paddresshelper=").append(dest.toBase64()).append("\">view</a>");
                        buf.append(" it in a browser (only when you're using the eepProxy)\n");
                        buf.append("<br />If you are going to share this on IRC, you need to split it up:<br />\n");
                        String str = dest.toBase64();
                        buf.append(str.substring(0, str.length()/2)).append("<br />\n");
                        buf.append(str.substring(str.length()/2)).append("<br />\n");
                        buf.append("You can also post it to <a href=\"http://forum.i2p/viewforum.php?f=16\">Eepsite announcement forum</a><br />");
                    }
                }
            }
        }
    }
  ****/
    
    /**
     *
     */
    public void log(String s) {
        synchronized (_messages) {
            _messages.add(s);
            while (_messages.size() > 10)
                _messages.remove(0);
        }
        if (_log.shouldLog(Log.INFO))
            _log.info(s);
    }
    
    /**
     * Pull off any messages that the I2PTunnel has produced 
     *
     * @return list of messages pulled off (each is a String, earliest first)
     */
    public List<String> clearMessages() { 
        List<String> rv;
        synchronized (_messages) {
            rv = new ArrayList<String>(_messages);
            _messages.clear();
        }
        return rv;
    }

    /**
     * @since 0.9.15
     */
    @Override
    public String toString() { 
        return "TC " + getType() + ' ' + getName() + " for " + _tunnel + ' ' + _state;
    }
}
