package net.i2p.i2ptunnel.web;
/*
 * free (adj.): unencumbered; not under the control of others
 * Written by jrandom in 2005 and released into the public domain 
 * with no warranty of any kind, either expressed or implied.  
 * It probably won't make your computer catch on fire, or eat 
 * your children, but it might.  Use at your own risk.
 *
 */

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
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.ConcurrentHashMap;

import net.i2p.I2PAppContext;
import net.i2p.app.ClientAppManager;
import net.i2p.app.Outproxy;
import net.i2p.client.I2PClient;
import net.i2p.data.Certificate;
import net.i2p.data.Destination;
import net.i2p.data.PrivateKeyFile;
import net.i2p.data.SessionKey;
import net.i2p.i2ptunnel.I2PTunnelConnectClient;
import net.i2p.i2ptunnel.I2PTunnelHTTPClient;
import net.i2p.i2ptunnel.I2PTunnelHTTPClientBase;
import net.i2p.i2ptunnel.I2PTunnelHTTPServer;
import net.i2p.i2ptunnel.I2PTunnelIRCClient;
import net.i2p.i2ptunnel.I2PTunnelServer;
import net.i2p.i2ptunnel.TunnelController;
import net.i2p.i2ptunnel.TunnelControllerGroup;
import net.i2p.util.Addresses;
import net.i2p.util.ConcurrentHashSet;
import net.i2p.util.FileUtil;
import net.i2p.util.Log;
import net.i2p.util.PasswordManager;
import net.i2p.util.SecureFile;

/**
 * Simple accessor for exposing tunnel info, but also an ugly form handler
 *
 * Warning - This class is not part of the i2ptunnel API,
 * it has been moved from the jar to the war.
 * Usage by classes outside of i2ptunnel.war is deprecated.
 */
public class IndexBean {
    protected final I2PAppContext _context;
    protected final Log _log;
    protected final TunnelControllerGroup _group;
    private final String _fatalError;
    private String _action;
    private int _tunnel;
    //private long _prevNonce;
    //private long _prevNonce2;
    private String _curNonce;
    //private long _nextNonce;

    private String _type;
    private String _name;
    private String _description;
    private String _i2cpHost;
    private String _i2cpPort;
    private String _tunnelDepth;
    private String _tunnelQuantity;
    private String _tunnelVariance;
    private String _tunnelBackupQuantity;
    private boolean _connectDelay;
    private String _customOptions;
    private String _proxyList;
    private String _port;
    private String _reachableBy;
    private String _targetDestination;
    private String _targetHost;
    private String _targetPort;
    private String _spoofedHost;
    private String _privKeyFile;
    private String _profile;
    private boolean _startOnLoad;
    private boolean _sharedClient;
    private boolean _removeConfirmed;
    private final Set<String> _booleanOptions;
    private final Map<String, String> _otherOptions;
    private int _hashCashValue;
    private int _certType;
    private String _certSigner;
    private String _newProxyUser;
    private String _newProxyPW;
    
    public static final int RUNNING = 1;
    public static final int STARTING = 2;
    public static final int NOT_RUNNING = 3;
    public static final int STANDBY = 4;
    
    //static final String PROP_NONCE = IndexBean.class.getName() + ".nonce";
    //static final String PROP_NONCE_OLD = PROP_NONCE + '2';
    /** 3 wasn't enough for some browsers. They are reloading the page for some reason - maybe HEAD? @since 0.8.1 */
    private static final int MAX_NONCES = 8;
    /** store nonces in a static FIFO instead of in System Properties @since 0.8.1 */
    private static final List<String> _nonces = new ArrayList<String>(MAX_NONCES + 1);

    static final String CLIENT_NICKNAME = "shared clients";
    public static final String PROP_THEME_NAME = "routerconsole.theme";
    public static final String DEFAULT_THEME = "light";
    public static final String PROP_CSS_DISABLED = "routerconsole.css.disabled";
    public static final String PROP_JS_DISABLED = "routerconsole.javascript.disabled";
    private static final String PROP_PW_ENABLE = "routerconsole.auth.enable";
    private static final String OPT = TunnelController.PFX_OPTION;
    
    public IndexBean() {
        _context = I2PAppContext.getGlobalContext();
        _log = _context.logManager().getLog(IndexBean.class);
        TunnelControllerGroup tcg;
        String error;
        try {
            tcg = TunnelControllerGroup.getInstance();
            error = tcg == null ? _("Tunnels are not initialized yet, please reload in two minutes.")
                                : null;
        } catch (IllegalArgumentException iae) {
            tcg = null;
            error = iae.toString();
        }
        _group = tcg;
        _fatalError = error;
        _tunnel = -1;
        _curNonce = "-1";
        addNonce();
        _booleanOptions = new ConcurrentHashSet<String>(4);
        _otherOptions = new ConcurrentHashMap<String, String>(4);
    }
    
    /**
     *  @since 0.9.4
     */
    public boolean isInitialized() {
        return _group != null;
    }

    public static String getNextNonce() {
        synchronized (_nonces) {
            return _nonces.get(0);
        }
    }

    public void setNonce(String nonce) {
        if ( (nonce == null) || (nonce.trim().length() <= 0) ) return;
        _curNonce = nonce;
    }

    /** add a random nonce to the head of the queue @since 0.8.1 */
    private void addNonce() {
        String nextNonce = Long.toString(_context.random().nextLong());
        synchronized (_nonces) {
            _nonces.add(0, nextNonce);
            int sz = _nonces.size();
            if (sz > MAX_NONCES)
                _nonces.remove(sz - 1);
        }
    }

    /** do we know this nonce? @since 0.8.1 */
    private static boolean haveNonce(String nonce) {
        synchronized (_nonces) {
            return _nonces.contains(nonce);
        }
    }

    public void setAction(String action) {
        if ( (action == null) || (action.trim().length() <= 0) ) return;
        _action = action;
    }

    public void setTunnel(String tunnel) {
        if ( (tunnel == null) || (tunnel.trim().length() <= 0) ) return;
        try {
            _tunnel = Integer.parseInt(tunnel);
        } catch (NumberFormatException nfe) {
            _tunnel = -1;
        }
    }
    
    private String processAction() {
        if ( (_action == null) || (_action.trim().length() <= 0) || ("Cancel".equals(_action)))
            return "";
        if (_group == null)
            return "Error - tunnels are not initialized yet";
        // If passwords are turned on, all is assumed good
        if (!_context.getBooleanProperty(PROP_PW_ENABLE) &&
            !haveNonce(_curNonce))
            return _("Invalid form submission, probably because you used the 'back' or 'reload' button on your browser. Please resubmit.")
                   + ' ' +
                   _("If the problem persists, verify that you have cookies enabled in your browser.");
        if ("Stop all".equals(_action)) 
            return stopAll();
        else if ("Start all".equals(_action))
            return startAll();
        else if ("Restart all".equals(_action))
            return restartAll();
        else if ("Reload configuration".equals(_action))
            return reloadConfig();
        else if ("stop".equals(_action))
            return stop();
        else if ("start".equals(_action))
            return start();
        else if ("Save changes".equals(_action) || // IE workaround:
                (_action.toLowerCase(Locale.US).indexOf("s</span>ave") >= 0))
            return saveChanges();
        else if ("Delete this proxy".equals(_action) || // IE workaround:
                (_action.toLowerCase(Locale.US).indexOf("d</span>elete") >= 0))
            return deleteTunnel();
        else if ("Estimate".equals(_action))
            return PrivateKeyFile.estimateHashCashTime(_hashCashValue);
        else if ("Modify".equals(_action))
            return modifyDestination();
        else if ("Generate".equals(_action))
            return generateNewEncryptionKey();
        else
            return "Action " + _action + " unknown";
    }

    private String stopAll() {
        List<String> msgs = _group.stopAllControllers();
        return getMessages(msgs);
    }

    private String startAll() {
        List<String> msgs = _group.startAllControllers();
        return getMessages(msgs);
    }

    private String restartAll() {
        List<String> msgs = _group.restartAllControllers();
        return getMessages(msgs);
    }

    private String reloadConfig() {
        _group.reloadControllers();
        return _("Configuration reloaded for all tunnels");
    }

    private String start() {
        if (_tunnel < 0) return "Invalid tunnel";
        
        List<TunnelController> controllers = _group.getControllers();
        if (_tunnel >= controllers.size()) return "Invalid tunnel";
        TunnelController controller = controllers.get(_tunnel);
        controller.startTunnelBackground();
        // give the messages a chance to make it to the window
        try { Thread.sleep(1000); } catch (InterruptedException ie) {}
        // and give them something to look at in any case
        return _("Starting tunnel") + ' ' + getTunnelName(_tunnel) + "&hellip;";
    }
    
    private String stop() {
        if (_tunnel < 0) return "Invalid tunnel";
        
        List<TunnelController> controllers = _group.getControllers();
        if (_tunnel >= controllers.size()) return "Invalid tunnel";
        TunnelController controller = controllers.get(_tunnel);
        controller.stopTunnel();
        // give the messages a chance to make it to the window
        try { Thread.sleep(1000); } catch (InterruptedException ie) {}
        // and give them something to look at in any case
        return _("Stopping tunnel") + ' ' + getTunnelName(_tunnel) + "&hellip;";
    }
    
    private String saveChanges() {
        // Get current tunnel controller
        TunnelController cur = getController(_tunnel);
        
        Properties config = getConfig();
        
        if (cur == null) {
            // creating new
            cur = new TunnelController(config, "", true);
            _group.addController(cur);
            if (cur.getStartOnLoad())
                cur.startTunnelBackground();
        } else {
            cur.setConfig(config, "");
        }
        // Only modify other shared tunnels
        // if the current tunnel is shared, and of supported type
        if (Boolean.parseBoolean(cur.getSharedClient()) && isClient(cur.getType())) {
            // all clients use the same I2CP session, and as such, use the same I2CP options
            List<TunnelController> controllers = _group.getControllers();

            for (int i = 0; i < controllers.size(); i++) {
                TunnelController c = controllers.get(i);

                // Current tunnel modified by user, skip
                if (c == cur) continue;

                // Only modify this non-current tunnel
                // if it belongs to a shared destination, and is of supported type
                if (Boolean.parseBoolean(c.getSharedClient()) && isClient(c.getType())) {
                    Properties cOpt = c.getConfig("");
                    if (_tunnelQuantity != null) {
                        cOpt.setProperty("option.inbound.quantity", _tunnelQuantity);
                        cOpt.setProperty("option.outbound.quantity", _tunnelQuantity);
                    }
                    if (_tunnelDepth != null) {
                        cOpt.setProperty("option.inbound.length", _tunnelDepth);
                        cOpt.setProperty("option.outbound.length", _tunnelDepth);
                    }
                    if (_tunnelVariance != null) {
                        cOpt.setProperty("option.inbound.lengthVariance", _tunnelVariance);
                        cOpt.setProperty("option.outbound.lengthVariance", _tunnelVariance);
                    }
                    if (_tunnelBackupQuantity != null) {
                        cOpt.setProperty("option.inbound.backupQuantity", _tunnelBackupQuantity);
                        cOpt.setProperty("option.outbound.backupQuantity", _tunnelBackupQuantity);
                    }
                    cOpt.setProperty("option.inbound.nickname", CLIENT_NICKNAME);
                    cOpt.setProperty("option.outbound.nickname", CLIENT_NICKNAME);
                    
                    c.setConfig(cOpt, "");
                }
            }
        }
        
        List<String> msgs = doSave();
        return getMessages(msgs);
    }

    private List<String> doSave() { 
        List<String> rv = _group.clearAllMessages();
        try {
            _group.saveConfig();
            rv.add(0, _("Configuration changes saved"));
        } catch (IOException ioe) {
            _log.error("Failed to save config file", ioe);
            rv.add(0, _("Failed to save configuration") + ": " + ioe.toString());
        }
        return rv;
    } 

    /**
     *  Stop the tunnel, delete from config,
     *  rename the private key file if in the default directory
     */
    private String deleteTunnel() {
        if (!_removeConfirmed)
            return "Please confirm removal";
        
        TunnelController cur = getController(_tunnel);
        if (cur == null)
            return "Invalid tunnel number";
        
        List<String> msgs = _group.removeController(cur);
        msgs.addAll(doSave());

        // Rename private key file if it was a default name in
        // the default directory, so it doesn't get reused when a new
        // tunnel is created.
        // Use configured file name if available, not the one from the form.
        String pk = cur.getPrivKeyFile();
        if (pk == null)
            pk = _privKeyFile;
        if (pk != null && pk.startsWith("i2ptunnel") && pk.endsWith("-privKeys.dat") &&
            ((!isClient(cur.getType())) || cur.getPersistentClientKey())) {
            File pkf = new File(_context.getConfigDir(), pk);
            if (pkf.exists()) {
                String name = cur.getName();
                if (name == null) {
                    name = cur.getDescription();
                    if (name == null) {
                        name = cur.getType();
                        if (name == null)
                            name = Long.toString(_context.clock().now());
                    }
                }
                name = "i2ptunnel-deleted-" + name.replace(' ', '_') + '-' + _context.clock().now() + "-privkeys.dat";
                File backupDir = new SecureFile(_context.getConfigDir(), TunnelController.KEY_BACKUP_DIR);
                File to;
                if (backupDir.isDirectory() || backupDir.mkdir())
                    to = new File(backupDir, name);
                else
                    to = new File(_context.getConfigDir(), name);
                boolean success = FileUtil.rename(pkf, to);
                if (success)
                    msgs.add("Private key file " + pkf.getAbsolutePath() +
                             " renamed to " + to.getAbsolutePath());
            }
        }
        return getMessages(msgs);
    }
    
    /**
     * Executes any action requested (start/stop/etc) and dump out the 
     * messages.
     *
     */
    public String getMessages() {
        if (_group == null)
            return _fatalError;
        
        StringBuilder buf = new StringBuilder(512);
        if (_action != null) {
            try {
                buf.append(processAction()).append("\n");
            } catch (Exception e) {
                _log.log(Log.CRIT, "Error processing " + _action, e);
            }
        }
        getMessages(_group.clearAllMessages(), buf);
        return buf.toString();
    }
    
    ////
    // The remaining methods are simple bean props for the jsp to query
    ////
    
    public String getTheme() {
        String theme = _context.getProperty(PROP_THEME_NAME, DEFAULT_THEME);
        return "/themes/console/" + theme + "/";
    }

    public boolean allowCSS() {
        String css = _context.getProperty(PROP_CSS_DISABLED);
        return (css == null);
    }
    
    public boolean allowJS() {
        String js = _context.getProperty(PROP_JS_DISABLED);
        return (js == null);
    }
    
    public int getTunnelCount() {
        if (_group == null) return 0;
        return _group.getControllers().size();
    }
    
    public boolean isClient(int tunnelNum) {
        TunnelController cur = getController(tunnelNum);
        if (cur == null) return false;
        return isClient(cur.getType());
    }

    public static boolean isClient(String type) {
        return ( (TunnelController.TYPE_STD_CLIENT.equals(type)) || 
        		(TunnelController.TYPE_HTTP_CLIENT.equals(type)) ||
        		(TunnelController.TYPE_SOCKS.equals(type)) ||
        		(TunnelController.TYPE_SOCKS_IRC.equals(type)) ||
        		(TunnelController.TYPE_CONNECT.equals(type)) ||
        		(TunnelController.TYPE_STREAMR_CLIENT.equals(type)) ||
        		(TunnelController.TYPE_IRC_CLIENT.equals(type)));
    }
    
    public String getTunnelName(int tunnel) {
        TunnelController tun = getController(tunnel);
        if (tun != null && tun.getName() != null)
            return tun.getName();
        else
            return _("New Tunnel");
    }
    
    /**
     *  No validation
     */
    public String getClientPort(int tunnel) {
        TunnelController tun = getController(tunnel);
        if (tun != null && tun.getListenPort() != null)
            return tun.getListenPort();
        else
            return "";
    }
    
    /**
     *  Returns error message if blank or invalid
     *  @since 0.9.3
     */
    public String getClientPort2(int tunnel) {
        TunnelController tun = getController(tunnel);
        if (tun != null && tun.getListenPort() != null) {
            String port = tun.getListenPort();
            if (port.length() == 0)
                return "<font color=\"red\">" + _("Port not set") + "</font>";
            int iport = Addresses.getPort(port);
            if (iport == 0)
                return "<font color=\"red\">" + _("Invalid port") + ' ' + port + "</font>";
            if (iport < 1024)
                return "<font color=\"red\">" +
                       _("Warning - ports less than 1024 are not recommended") +
                       ": " + port + "</font>";
            // dup check, O(n**2)
            List<TunnelController> controllers = _group.getControllers();
            for (int i = 0; i < controllers.size(); i++) {
                if (i == tunnel)
                    continue;
                if (port.equals(controllers.get(i).getListenPort()))
                    return "<font color=\"red\">" +
                           _("Warning - duplicate port") +
                           ": " + port + "</font>";
            }
            return port;
        }
        return "<font color=\"red\">" + _("Port not set") + "</font>";
    }
    
    public String getTunnelType(int tunnel) {
        TunnelController tun = getController(tunnel);
        if (tun != null)
            return getTypeName(tun.getType());
        else
            return "";
    }
    
    public String getTypeName(String internalType) {
        if (TunnelController.TYPE_STD_CLIENT.equals(internalType)) return _("Standard client");
        else if (TunnelController.TYPE_HTTP_CLIENT.equals(internalType)) return _("HTTP/HTTPS client");
        else if (TunnelController.TYPE_IRC_CLIENT.equals(internalType)) return _("IRC client");
        else if (TunnelController.TYPE_STD_SERVER.equals(internalType)) return _("Standard server");
        else if (TunnelController.TYPE_HTTP_SERVER.equals(internalType)) return _("HTTP server");
        else if (TunnelController.TYPE_SOCKS.equals(internalType)) return _("SOCKS 4/4a/5 proxy");
        else if (TunnelController.TYPE_SOCKS_IRC.equals(internalType)) return _("SOCKS IRC proxy");
        else if (TunnelController.TYPE_CONNECT.equals(internalType)) return _("CONNECT/SSL/HTTPS proxy");
        else if (TunnelController.TYPE_IRC_SERVER.equals(internalType)) return _("IRC server");
        else if (TunnelController.TYPE_STREAMR_CLIENT.equals(internalType)) return _("Streamr client");
        else if (TunnelController.TYPE_STREAMR_SERVER.equals(internalType)) return _("Streamr server");
        else if (TunnelController.TYPE_HTTP_BIDIR_SERVER.equals(internalType)) return _("HTTP bidir");
        else return internalType;
    }
    
    public String getInternalType(int tunnel) {
        TunnelController tun = getController(tunnel);
        if (tun != null)
            return tun.getType();
        else
            return "";
    }
    
    public String getClientInterface(int tunnel) {
        TunnelController tun = getController(tunnel);
        if (tun != null) {
            if ("streamrclient".equals(tun.getType()))
                return tun.getTargetHost();
            else
                return tun.getListenOnInterface();
        } else
            return "127.0.0.1";
    }
    
    public int getTunnelStatus(int tunnel) {
        TunnelController tun = getController(tunnel);
        if (tun == null) return NOT_RUNNING;
        if (tun.getIsRunning()) {
            if (isClient(tunnel) && tun.getIsStandby())
                return STANDBY;
            else
                return RUNNING;
        } else if (tun.getIsStarting()) return STARTING;
        else return NOT_RUNNING;
    }
    
    public String getTunnelDescription(int tunnel) {
        TunnelController tun = getController(tunnel);
        if (tun != null && tun.getDescription() != null)
            return tun.getDescription();
        else
            return "";
    }
    
    public String getSharedClient(int tunnel) {
    	TunnelController tun = getController(tunnel);
    	if (tun != null)
    		return tun.getSharedClient();
    	else
    		return "";
    }
    
    public String getClientDestination(int tunnel) {
        TunnelController tun = getController(tunnel);
        if (tun == null) return "";
        String rv;
        if (TunnelController.TYPE_STD_CLIENT.equals(tun.getType()) ||
            TunnelController.TYPE_IRC_CLIENT.equals(tun.getType()) ||
            TunnelController.TYPE_STREAMR_CLIENT.equals(tun.getType()))
            rv = tun.getTargetDestination();
        else
            rv = tun.getProxyList();
        return rv != null ? rv : "";
    }
    
    /**
     * Call this to see if it is ok to linkify getServerTarget()
     * @since 0.8.3
     */
    public boolean isServerTargetLinkValid(int tunnel) {
        TunnelController tun = getController(tunnel);
        return tun != null &&
               TunnelController.TYPE_HTTP_SERVER.equals(tun.getType()) &&
               tun.getTargetHost() != null &&
               tun.getTargetPort() != null;
    }

    /**
     * @return valid host:port only if isServerTargetLinkValid() is true
     */
    public String getServerTarget(int tunnel) {
        TunnelController tun = getController(tunnel);
        if (tun != null) {
            String host;
            if ("streamrserver".equals(tun.getType()))
                host = tun.getListenOnInterface();
            else
                host = tun.getTargetHost();
            String port = tun.getTargetPort();
            if (host == null || host.length() == 0)
                host = "<font color=\"red\">" + _("Host not set") + "</font>";
            else if (Addresses.getIP(host) == null)
                host = "<font color=\"red\">" + _("Invalid address") + ' ' + host + "</font>";
            else if (host.indexOf(':') >= 0)
                host = '[' + host + ']';
            if (port == null || port.length() == 0)
                port = "<font color=\"red\">" + _("Port not set") + "</font>";
            else if (Addresses.getPort(port) == 0)
                port = "<font color=\"red\">" + _("Invalid port") + ' ' + port + "</font>";
            return host + ':' + port;
       }  else
            return "";
    }
    
    public String getDestinationBase64(int tunnel) {
        TunnelController tun = getController(tunnel);
        if (tun != null) {
            String rv = tun.getMyDestination();
            if (rv != null)
                return rv;
            // if not running, do this the hard way
            String keyFile = tun.getPrivKeyFile();
            if (keyFile != null && keyFile.trim().length() > 0) {
                PrivateKeyFile pkf = new PrivateKeyFile(keyFile);
                try {
                    Destination d = pkf.getDestination();
                    if (d != null)
                        return d.toBase64();
                } catch (Exception e) {}
            }
        }
        return "";
    }
    
    /**
     *  @return "{52 chars}.b32.i2p" or ""
     */
    public String getDestHashBase32(int tunnel) {
        TunnelController tun = getController(tunnel);
        if (tun != null) {
            String rv = tun.getMyDestHashBase32();
            if (rv != null)
                return rv;
        }
        return "";
    }
    
    /**
     *  For index.jsp
     *  @return true if the plugin is enabled, installed, and running
     *  @since 0.9.11
     */
    public boolean getIsUsingOutproxyPlugin(int tunnel) {
        TunnelController tun = getController(tunnel);
        if (tun != null) {
            if (TunnelController.TYPE_HTTP_CLIENT.equals(tun.getType())) {
                Properties opts = tun.getClientOptionProps();
                if (Boolean.parseBoolean(opts.getProperty(I2PTunnelHTTPClient.PROP_USE_OUTPROXY_PLUGIN, "true"))) {
                    ClientAppManager mgr = _context.clientAppManager();
                    if (mgr != null)
                        return mgr.getRegisteredApp(Outproxy.NAME) != null;
                }
            }
        }
        return false;
    }

    ///
    /// bean props for form submission
    ///
    
    /**
     * What type of tunnel (httpclient, ircclient, client, or server).  This is 
     * required when adding a new tunnel.
     *
     */
    public void setType(String type) { 
        _type = (type != null ? type.trim() : null);   
    }
    String getType() { return _type; }
    
    /** Short name of the tunnel */
    public void setName(String name) { 
        _name = (name != null ? name.trim() : null);
    }
    /** one line description */
    public void setDescription(String description) { 
        _description = (description != null ? description.trim() : null);
    }
    /** I2CP host the router is on, ignored when in router context */
    public void setClientHost(String host) {
        _i2cpHost = (host != null ? host.trim() : null);
    }
    /** I2CP port the router is on, ignored when in router context */
    public void setClientport(String port) {
        _i2cpPort = (port != null ? port.trim() : null);
    }
    /** how many hops to use for inbound tunnels */
    public void setTunnelDepth(String tunnelDepth) { 
        _tunnelDepth = (tunnelDepth != null ? tunnelDepth.trim() : null);
    }
    /** how many parallel inbound tunnels to use */
    public void setTunnelQuantity(String tunnelQuantity) { 
        _tunnelQuantity = (tunnelQuantity != null ? tunnelQuantity.trim() : null);
    }
    /** how much randomisation to apply to the depth of tunnels */
    public void setTunnelVariance(String tunnelVariance) { 
        _tunnelVariance = (tunnelVariance != null ? tunnelVariance.trim() : null);
    }
    /** how many tunnels to hold in reserve to guard against failures */
    public void setTunnelBackupQuantity(String tunnelBackupQuantity) { 
        _tunnelBackupQuantity = (tunnelBackupQuantity != null ? tunnelBackupQuantity.trim() : null);
    }
    /** what I2P session overrides should be used */
    public void setNofilter_customOptions(String customOptions) { 
        _customOptions = (customOptions != null ? customOptions.trim() : null);
    }
    /** what HTTP outproxies should be used (httpclient specific) */
    public void setProxyList(String proxyList) { 
        _proxyList = (proxyList != null ? proxyList.trim() : null);
    }
    /** what port should this client/httpclient/ircclient listen on */
    public void setPort(String port) { 
        _port = (port != null ? port.trim() : null);
    }
    /** 
     * what interface should this client/httpclient/ircclient listen on
     */
    public void setReachableBy(String reachableBy) { 
        _reachableBy = (reachableBy != null ? reachableBy.trim() : null);
    }
    /** What peer does this client tunnel point at */
    public void setTargetDestination(String dest) { 
        _targetDestination = (dest != null ? dest.trim() : null);
    }
    /** What host does this server tunnel point at */
    public void setTargetHost(String host) { 
        _targetHost = (host != null ? host.trim() : null);
    }
    /** What port does this server tunnel point at */
    public void setTargetPort(String port) { 
        _targetPort = (port != null ? port.trim() : null);
    }
    /** What host does this http server tunnel spoof */
    public void setSpoofedHost(String host) { 
        _spoofedHost = (host != null ? host.trim() : null);
    }
    /** What filename is this server tunnel's private keys stored in */
    public void setPrivKeyFile(String file) { 
        _privKeyFile = (file != null ? file.trim() : null);
    }
    /**
     * If called with any value (and the form submitted with action=Remove),
     * we really do want to stop and remove the tunnel.
     */
    public void setRemoveConfirm(String moo) {
        _removeConfirmed = true;
    }
    /**
     * If called with any value, we want this tunnel to start whenever it is
     * loaded (aka right now and whenever the router is started up)
     */
    public void setStartOnLoad(String moo) {
        _startOnLoad = true;
    }
    public void setShared(String moo) {
    	_sharedClient=true;
    }
    public void setShared(boolean val) {
    	_sharedClient=val;
    }
    public void setConnectDelay(String moo) {
        _connectDelay = true;
    }
    public void setProfile(String profile) { 
        _profile = profile; 
    }

    public void setReduce(String moo) {
        _booleanOptions.add("i2cp.reduceOnIdle");
    }
    public void setClose(String moo) {
        _booleanOptions.add("i2cp.closeOnIdle");
    }
    public void setEncrypt(String moo) {
        _booleanOptions.add("i2cp.encryptLeaseSet");
    }

    /** @since 0.8.9 */
    public void setDCC(String moo) {
        _booleanOptions.add(I2PTunnelIRCClient.PROP_DCC);
    }

    /** @since 0.9.9 */
    public void setUseSSL(String moo) {
        _booleanOptions.add(I2PTunnelServer.PROP_USE_SSL);
    }

    /** @since 0.9.9 */
    public boolean isSSLEnabled(int tunnel) {
        TunnelController tun = getController(tunnel);
        if (tun != null) {
            Properties opts = tun.getClientOptionProps();
            return Boolean.parseBoolean(opts.getProperty(I2PTunnelServer.PROP_USE_SSL));
        }
        return false;
    }

    /** @since 0.9.12 */
    public void setRejectInproxy(String moo) {
        _booleanOptions.add(I2PTunnelHTTPServer.OPT_REJECT_INPROXY);
    }

    /** @since 0.9.12 */
    public boolean isRejectInproxy(int tunnel) {
        TunnelController tun = getController(tunnel);
        if (tun != null) {
            Properties opts = tun.getClientOptionProps();
            return Boolean.parseBoolean(opts.getProperty(I2PTunnelHTTPServer.OPT_REJECT_INPROXY));
        }
        return false;
    }

    /** @since 0.9.13 */
    public void setUniqueLocal(String moo) {
        _booleanOptions.add(I2PTunnelServer.PROP_UNIQUE_LOCAL);
    }

    protected static final String PROP_ENABLE_ACCESS_LIST = "i2cp.enableAccessList";
    protected static final String PROP_ENABLE_BLACKLIST = "i2cp.enableBlackList";

    public void setAccessMode(String val) {
        if ("1".equals(val))
            _booleanOptions.add(PROP_ENABLE_ACCESS_LIST);
        else if ("2".equals(val))
            _booleanOptions.add(PROP_ENABLE_BLACKLIST);
    }

    public void setDelayOpen(String moo) {
        _booleanOptions.add("i2cp.delayOpen");
    }
    public void setNewDest(String val) {
        if ("1".equals(val))
            _booleanOptions.add("i2cp.newDestOnResume");
        else if ("2".equals(val))
            _booleanOptions.add("persistentClientKey");
    }

    public void setReduceTime(String val) {
        if (val != null) {
            try {
                _otherOptions.put("i2cp.reduceIdleTime", Integer.toString(Integer.parseInt(val.trim()) * 60*1000));
            } catch (NumberFormatException nfe) {}
        }
    }
    public void setReduceCount(String val) {
        if (val != null)
            _otherOptions.put("i2cp.reduceQuantity", val.trim());
    }
    public void setEncryptKey(String val) {
        if (val != null)
            _otherOptions.put("i2cp.leaseSetKey", val.trim());
    }

    public void setAccessList(String val) {
        if (val != null)
            _otherOptions.put("i2cp.accessList", val.trim().replace("\r\n", ",").replace("\n", ",").replace(" ", ","));
    }

    public void setJumpList(String val) {
        if (val != null)
            _otherOptions.put(I2PTunnelHTTPClient.PROP_JUMP_SERVERS, val.trim().replace("\r\n", ",").replace("\n", ",").replace(" ", ","));
    }

    public void setCloseTime(String val) {
        if (val != null) {
            try {
                _otherOptions.put("i2cp.closeIdleTime", Integer.toString(Integer.parseInt(val.trim()) * 60*1000));
            } catch (NumberFormatException nfe) {}
        }
    }

    /** @since 0.9.14 */
    public void setAllowUserAgent(String moo) {
        _booleanOptions.add(I2PTunnelHTTPClient.PROP_USER_AGENT);
    }

    /** @since 0.9.14 */
    public void setAllowReferer(String moo) {
        _booleanOptions.add(I2PTunnelHTTPClient.PROP_REFERER);
    }

    /** @since 0.9.14 */
    public void setAllowAccept(String moo) {
        _booleanOptions.add(I2PTunnelHTTPClient.PROP_ACCEPT);
    }

    /** @since 0.9.14 */
    public void setAllowInternalSSL(String moo) {
        _booleanOptions.add(I2PTunnelHTTPClient.PROP_INTERNAL_SSL);
    }

    /** all proxy auth @since 0.8.2 */
    public void setProxyAuth(String s) {
        if (s != null)
            _otherOptions.put(I2PTunnelHTTPClientBase.PROP_AUTH, I2PTunnelHTTPClientBase.DIGEST_AUTH);
    }
    
    public void setProxyUsername(String s) {
        if (s != null)
            _newProxyUser = s.trim();
    }
    
    public void setNofilter_proxyPassword(String s) {
        if (s != null)
            _newProxyPW = s.trim();
    }
    
    public void setOutproxyAuth(String s) {
        _otherOptions.put(I2PTunnelHTTPClientBase.PROP_OUTPROXY_AUTH, I2PTunnelHTTPClientBase.DIGEST_AUTH);
    }
    
    public void setOutproxyUsername(String s) {
        if (s != null)
            _otherOptions.put(I2PTunnelHTTPClientBase.PROP_OUTPROXY_USER, s.trim());
    }
    
    public void setNofilter_outproxyPassword(String s) {
        if (s != null)
            _otherOptions.put(I2PTunnelHTTPClientBase.PROP_OUTPROXY_PW, s.trim());
    }

    /** @since 0.9.11 */
    public void setSslProxies(String s) {
        if (s != null)
            _otherOptions.put(I2PTunnelHTTPClient.PROP_SSL_OUTPROXIES, s.trim().replace(" ", ","));
    }

    /** @since 0.9.11 */
    public void setUseOutproxyPlugin(String moo) {
        _booleanOptions.add(I2PTunnelHTTPClient.PROP_USE_OUTPROXY_PLUGIN);
    }
    
    /** all of these are @since 0.8.3 */
    protected static final String PROP_MAX_CONNS_MIN = "i2p.streaming.maxConnsPerMinute";
    protected static final String PROP_MAX_CONNS_HOUR = "i2p.streaming.maxConnsPerHour";
    protected static final String PROP_MAX_CONNS_DAY = "i2p.streaming.maxConnsPerDay";
    protected static final String PROP_MAX_TOTAL_CONNS_MIN = "i2p.streaming.maxTotalConnsPerMinute";
    protected static final String PROP_MAX_TOTAL_CONNS_HOUR = "i2p.streaming.maxTotalConnsPerHour";
    protected static final String PROP_MAX_TOTAL_CONNS_DAY = "i2p.streaming.maxTotalConnsPerDay";
    protected static final String PROP_MAX_STREAMS = "i2p.streaming.maxConcurrentStreams";

    public void setLimitMinute(String s) {
        if (s != null)
            _otherOptions.put(PROP_MAX_CONNS_MIN, s.trim());
    }

    public void setLimitHour(String s) {
        if (s != null)
            _otherOptions.put(PROP_MAX_CONNS_HOUR, s.trim());
    }

    public void setLimitDay(String s) {
        if (s != null)
            _otherOptions.put(PROP_MAX_CONNS_DAY, s.trim());
    }

    public void setTotalMinute(String s) {
        if (s != null)
            _otherOptions.put(PROP_MAX_TOTAL_CONNS_MIN, s.trim());
    }

    public void setTotalHour(String s) {
        if (s != null)
            _otherOptions.put(PROP_MAX_TOTAL_CONNS_HOUR, s.trim());
    }

    public void setTotalDay(String s) {
        if (s != null)
            _otherOptions.put(PROP_MAX_TOTAL_CONNS_DAY, s.trim());
    }

    public void setMaxStreams(String s) {
        if (s != null)
            _otherOptions.put(PROP_MAX_STREAMS, s.trim());
    }

    /**
     * POST limits
     * @since 0.9.9
     */
    public void setPostMax(String s) {
        if (s != null)
            _otherOptions.put(I2PTunnelHTTPServer.OPT_POST_MAX, s.trim());
    }

    public void setPostTotalMax(String s) {
        if (s != null)
            _otherOptions.put(I2PTunnelHTTPServer.OPT_POST_TOTAL_MAX, s.trim());
    }

    public void setPostCheckTime(String s) {
        if (s != null)
            _otherOptions.put(I2PTunnelHTTPServer.OPT_POST_WINDOW, Integer.toString(Integer.parseInt(s.trim()) * 60));
    }

    public void setPostBanTime(String s) {
        if (s != null)
            _otherOptions.put(I2PTunnelHTTPServer.OPT_POST_BAN_TIME, Integer.toString(Integer.parseInt(s.trim()) * 60));
    }

    public void setPostTotalBanTime(String s) {
        if (s != null)
            _otherOptions.put(I2PTunnelHTTPServer.OPT_POST_TOTAL_BAN_TIME, Integer.toString(Integer.parseInt(s.trim()) * 60));
    }

    /** params needed for hashcash and dest modification */
    public void setEffort(String val) {
        if (val != null) {
            try {
                _hashCashValue = Integer.parseInt(val.trim());
            } catch (NumberFormatException nfe) {}
        }
    }

    public void setCert(String val) {
        if (val != null) {
            try {
                _certType = Integer.parseInt(val.trim());
            } catch (NumberFormatException nfe) {}
        }
    }

    public void setSigner(String val) {
        _certSigner = val;
    }

    /** @since 0.9.12 */
    public void setSigType(String val) {
        if (val != null) {
            _otherOptions.put(I2PClient.PROP_SIGTYPE, val);
            if (val.equals("0"))
                _certType = 0;
            else
                _certType = 5;
        }
        // TODO: Call modifyDestination??
        // Otherwise this only works on a new tunnel...
    }

    /** Modify or create a destination */
    private String modifyDestination() {
        if (_privKeyFile == null || _privKeyFile.trim().length() <= 0)
            return "Private Key File not specified";

        TunnelController tun = getController(_tunnel);
        Properties config = getConfig();
        if (tun == null) {
            // creating new
            tun = new TunnelController(config, "", true);
            _group.addController(tun);
            saveChanges();
        } else if (tun.getIsRunning() || tun.getIsStarting()) {
            return "Tunnel must be stopped before modifying destination";
        }

        File keyFile = new File(_privKeyFile);
        if (!keyFile.isAbsolute())
            keyFile = new File(_context.getConfigDir(), _privKeyFile);
        PrivateKeyFile pkf = new PrivateKeyFile(keyFile);
        try {
            pkf.createIfAbsent();
        } catch (Exception e) {
            return "Create private key file failed: " + e;
        }
        switch (_certType) {
            case Certificate.CERTIFICATE_TYPE_NULL:
            case Certificate.CERTIFICATE_TYPE_HIDDEN:
                pkf.setCertType(_certType);
                break;
            case Certificate.CERTIFICATE_TYPE_HASHCASH:
                pkf.setHashCashCert(_hashCashValue);
                break;
            case Certificate.CERTIFICATE_TYPE_SIGNED:
                if (_certSigner == null || _certSigner.trim().length() <= 0)
                    return "No signing destination specified";
                // find the signer's key file...
                String signerPKF = null;
                for (int i = 0; i < getTunnelCount(); i++) {
                    TunnelController c = getController(i);
                    if (_certSigner.equals(c.getConfig("").getProperty(TunnelController.PROP_NAME)) ||
                        _certSigner.equals(c.getConfig("").getProperty(TunnelController.PROP_SPOOFED_HOST))) {
                        signerPKF = c.getConfig("").getProperty(TunnelController.PROP_FILE);
                        break;
                    }
                }
                if (signerPKF == null || signerPKF.length() <= 0)
                    return "Signing destination " + _certSigner + " not found";
                if (_privKeyFile.equals(signerPKF))
                    return "Self-signed destinations not allowed";
                Certificate c = pkf.setSignedCert(new PrivateKeyFile(signerPKF));
                if (c == null)
                    return "Signing failed - does signer destination exist?";
                break;
            default:
                return "Unknown certificate type";
        }
        Destination newdest;
        try {
            pkf.write();
            newdest = pkf.getDestination();
        } catch (Exception e) {
            return "Modification failed: " + e;
        }
        return "Destination modified - " +
               "New Base32 is " + newdest.toBase32() +
               "New Destination is " + newdest.toBase64();
     }

    /** New key */
    private String generateNewEncryptionKey() {
        TunnelController tun = getController(_tunnel);
        Properties config = getConfig();
        if (tun == null) {
            // creating new
            tun = new TunnelController(config, "", true);
            _group.addController(tun);
            saveChanges();
        } else if (tun.getIsRunning() || tun.getIsStarting()) {
            return "Tunnel must be stopped before modifying leaseset encryption key";
        }
        byte[] data = new byte[SessionKey.KEYSIZE_BYTES];
        _context.random().nextBytes(data);
        SessionKey sk = new SessionKey(data);
        setEncryptKey(sk.toBase64());
        setEncrypt("");
        saveChanges();
        return "New Leaseset Encryption Key: " + sk.toBase64();
     }

    /**
     * Based on all provided data, create a set of configuration parameters 
     * suitable for use in a TunnelController.  This will replace (not add to)
     * any existing parameters, so this should return a comprehensive mapping.
     *
     */
    private Properties getConfig() {
        Properties config = new Properties();
        updateConfigGeneric(config);
        
        if ((isClient(_type) && !TunnelController.TYPE_STREAMR_CLIENT.equals(_type)) ||
            TunnelController.TYPE_STREAMR_SERVER.equals(_type)) {
            // streamrserver uses interface
            if (_reachableBy != null)
                config.setProperty(TunnelController.PROP_INTFC, _reachableBy);
            else
                config.setProperty(TunnelController.PROP_INTFC, "");
        } else {
            // streamrclient uses targetHost
            if (_targetHost != null)
                config.setProperty(TunnelController.PROP_TARGET_HOST, _targetHost);
        }

        if (isClient(_type)) {
            // generic client stuff
            if (_port != null)
                config.setProperty(TunnelController.PROP_LISTEN_PORT, _port);
            config.setProperty(TunnelController.PROP_SHARED, _sharedClient + "");
            for (String p : _booleanClientOpts)
                config.setProperty(OPT + p, "" + _booleanOptions.contains(p));
            for (String p : _otherClientOpts)
                if (_otherOptions.containsKey(p))
                    config.setProperty(OPT + p, _otherOptions.get(p));
        } else {
            // generic server stuff
            if (_targetPort != null)
                config.setProperty(TunnelController.PROP_TARGET_PORT, _targetPort);
            for (String p : _booleanServerOpts)
                config.setProperty(OPT + p, "" + _booleanOptions.contains(p));
            for (String p : _otherServerOpts)
                if (_otherOptions.containsKey(p))
                    config.setProperty(OPT + p, _otherOptions.get(p));
        }

        // generic proxy stuff
        if (TunnelController.TYPE_HTTP_CLIENT.equals(_type) || TunnelController.TYPE_CONNECT.equals(_type) || 
            TunnelController.TYPE_SOCKS.equals(_type) ||TunnelController.TYPE_SOCKS_IRC.equals(_type)) {
            for (String p : _booleanProxyOpts)
                config.setProperty(OPT + p, "" + _booleanOptions.contains(p));
            if (_proxyList != null)
                config.setProperty(TunnelController.PROP_PROXIES, _proxyList);
        }

        // Proxy auth including migration to MD5
        if (TunnelController.TYPE_HTTP_CLIENT.equals(_type) || TunnelController.TYPE_CONNECT.equals(_type)) {
            // Migrate even if auth is disabled
            // go get the old from custom options that updateConfigGeneric() put in there
            String puser = OPT + I2PTunnelHTTPClientBase.PROP_USER;
            String user = config.getProperty(puser);
            String ppw = OPT + I2PTunnelHTTPClientBase.PROP_PW;
            String pw = config.getProperty(ppw);
            if (user != null && pw != null && user.length() > 0 && pw.length() > 0) {
                String pmd5 = OPT + I2PTunnelHTTPClientBase.PROP_PROXY_DIGEST_PREFIX +
                              user + I2PTunnelHTTPClientBase.PROP_PROXY_DIGEST_SUFFIX;
                if (config.getProperty(pmd5) == null) {
                    // not in there, migrate
                    String realm = _type.equals(TunnelController.TYPE_HTTP_CLIENT) ? I2PTunnelHTTPClient.AUTH_REALM
                                                              : I2PTunnelConnectClient.AUTH_REALM;
                    String hex = PasswordManager.md5Hex(realm, user, pw);
                    if (hex != null) {
                        config.setProperty(pmd5, hex);
                        config.remove(puser);
                        config.remove(ppw);
                    }
                }
            }
            // New user/password
            String auth = _otherOptions.get(I2PTunnelHTTPClientBase.PROP_AUTH);
            if (auth != null && !auth.equals("false")) {
                if (_newProxyUser != null && _newProxyPW != null &&
                    _newProxyUser.length() > 0 && _newProxyPW.length() > 0) {
                    String pmd5 = OPT + I2PTunnelHTTPClientBase.PROP_PROXY_DIGEST_PREFIX +
                                  _newProxyUser + I2PTunnelHTTPClientBase.PROP_PROXY_DIGEST_SUFFIX;
                    String realm = _type.equals(TunnelController.TYPE_HTTP_CLIENT) ? I2PTunnelHTTPClient.AUTH_REALM
                                                              : I2PTunnelConnectClient.AUTH_REALM;
                    String hex = PasswordManager.md5Hex(realm, _newProxyUser, _newProxyPW);
                    if (hex != null)
                        config.setProperty(pmd5, hex);
                }
            }
        }

        if (TunnelController.TYPE_IRC_CLIENT.equals(_type) ||
            TunnelController.TYPE_STD_CLIENT.equals(_type) ||
            TunnelController.TYPE_STREAMR_CLIENT.equals(_type)) {
            if (_targetDestination != null)
                config.setProperty(TunnelController.PROP_DEST, _targetDestination);
        } else if (TunnelController.TYPE_HTTP_SERVER.equals(_type) ||
                   TunnelController.TYPE_HTTP_BIDIR_SERVER.equals(_type)) {
            if (_spoofedHost != null)
                config.setProperty(TunnelController.PROP_SPOOFED_HOST, _spoofedHost);
            for (String p : _httpServerOpts)
                if (_otherOptions.containsKey(p))
                    config.setProperty(OPT + p, _otherOptions.get(p));
        }
        if (TunnelController.TYPE_HTTP_BIDIR_SERVER.equals(_type)) {
            if (_port != null)
                config.setProperty(TunnelController.PROP_LISTEN_PORT, _port);
            if (_reachableBy != null)
                config.setProperty(TunnelController.PROP_INTFC, _reachableBy);
            else if (_targetHost != null)
                config.setProperty(TunnelController.PROP_INTFC, _targetHost);
            else
                config.setProperty(TunnelController.PROP_INTFC, "");
        }

        if (TunnelController.TYPE_IRC_CLIENT.equals(_type)) {
            boolean dcc = _booleanOptions.contains(I2PTunnelIRCClient.PROP_DCC);
            config.setProperty(OPT + I2PTunnelIRCClient.PROP_DCC,
                               "" + dcc);
            // add some sane server options since they aren't in the GUI (yet)
            if (dcc) {
                config.setProperty(OPT + PROP_MAX_CONNS_MIN, "3");
                config.setProperty(OPT + PROP_MAX_CONNS_HOUR, "10");
                config.setProperty(OPT + PROP_MAX_TOTAL_CONNS_MIN, "5");
                config.setProperty(OPT + PROP_MAX_TOTAL_CONNS_HOUR, "25");
            }
        }

        return config;
    }
    
    private static final String _noShowOpts[] = {
        "inbound.length", "outbound.length", "inbound.lengthVariance", "outbound.lengthVariance",
        "inbound.backupQuantity", "outbound.backupQuantity", "inbound.quantity", "outbound.quantity",
        "inbound.nickname", "outbound.nickname", "i2p.streaming.connectDelay", "i2p.streaming.maxWindowSize",
        I2PTunnelIRCClient.PROP_DCC
        };
    private static final String _booleanClientOpts[] = {
        "i2cp.reduceOnIdle", "i2cp.closeOnIdle", "i2cp.newDestOnResume", "persistentClientKey", "i2cp.delayOpen"
        };
    private static final String _booleanProxyOpts[] = {
        I2PTunnelHTTPClientBase.PROP_OUTPROXY_AUTH,
        I2PTunnelHTTPClient.PROP_USE_OUTPROXY_PLUGIN,
        I2PTunnelHTTPClient.PROP_USER_AGENT,
        I2PTunnelHTTPClient.PROP_REFERER,
        I2PTunnelHTTPClient.PROP_ACCEPT,
        I2PTunnelHTTPClient.PROP_INTERNAL_SSL
        };
    private static final String _booleanServerOpts[] = {
        "i2cp.reduceOnIdle", "i2cp.encryptLeaseSet", PROP_ENABLE_ACCESS_LIST, PROP_ENABLE_BLACKLIST,
        I2PTunnelServer.PROP_USE_SSL,
        I2PTunnelHTTPServer.OPT_REJECT_INPROXY,
        I2PTunnelServer.PROP_UNIQUE_LOCAL
        };
    private static final String _otherClientOpts[] = {
        "i2cp.reduceIdleTime", "i2cp.reduceQuantity", "i2cp.closeIdleTime",
        "outproxyUsername", "outproxyPassword",
        I2PTunnelHTTPClient.PROP_JUMP_SERVERS,
        I2PTunnelHTTPClientBase.PROP_AUTH,
        I2PClient.PROP_SIGTYPE,
        I2PTunnelHTTPClient.PROP_SSL_OUTPROXIES
        };
    private static final String _otherServerOpts[] = {
        "i2cp.reduceIdleTime", "i2cp.reduceQuantity", "i2cp.leaseSetKey", "i2cp.accessList",
         PROP_MAX_CONNS_MIN, PROP_MAX_CONNS_HOUR, PROP_MAX_CONNS_DAY,
         PROP_MAX_TOTAL_CONNS_MIN, PROP_MAX_TOTAL_CONNS_HOUR, PROP_MAX_TOTAL_CONNS_DAY,
         PROP_MAX_STREAMS, I2PClient.PROP_SIGTYPE
        };
    private static final String _httpServerOpts[] = {
        I2PTunnelHTTPServer.OPT_POST_WINDOW,
        I2PTunnelHTTPServer.OPT_POST_BAN_TIME,
        I2PTunnelHTTPServer.OPT_POST_TOTAL_BAN_TIME,
        I2PTunnelHTTPServer.OPT_POST_MAX,
        I2PTunnelHTTPServer.OPT_POST_TOTAL_MAX
        };

    /**
     *  do NOT add these to noShoOpts, we must leave them in for HTTPClient and ConnectCLient
     *  so they will get migrated to MD5
     *  TODO migrate socks to MD5
     */
    private static final String _otherProxyOpts[] = {
        "proxyUsername", "proxyPassword"
        };

    protected static final Set<String> _noShowSet = new HashSet<String>(128);
    protected static final Set<String> _nonProxyNoShowSet = new HashSet<String>(4);
    static {
        _noShowSet.addAll(Arrays.asList(_noShowOpts));
        _noShowSet.addAll(Arrays.asList(_booleanClientOpts));
        _noShowSet.addAll(Arrays.asList(_booleanProxyOpts));
        _noShowSet.addAll(Arrays.asList(_booleanServerOpts));
        _noShowSet.addAll(Arrays.asList(_otherClientOpts));
        _noShowSet.addAll(Arrays.asList(_otherServerOpts));
        _noShowSet.addAll(Arrays.asList(_httpServerOpts));
        _nonProxyNoShowSet.addAll(Arrays.asList(_otherProxyOpts));
    }

    private void updateConfigGeneric(Properties config) {
        config.setProperty(TunnelController.PROP_TYPE, _type);
        if (_name != null)
            config.setProperty(TunnelController.PROP_NAME, _name);
        if (_description != null)
            config.setProperty(TunnelController.PROP_DESCR, _description);
        if (!_context.isRouterContext()) {
            if (_i2cpHost != null)
                config.setProperty(TunnelController.PROP_I2CP_HOST, _i2cpHost);
            if ( (_i2cpPort != null) && (_i2cpPort.trim().length() > 0) ) {
                config.setProperty(TunnelController.PROP_I2CP_PORT, _i2cpPort);
            } else {
                config.setProperty(TunnelController.PROP_I2CP_PORT, "7654");
            }
        }
        if (_privKeyFile != null)
            config.setProperty(TunnelController.PROP_FILE, _privKeyFile);
        
        if (_customOptions != null) {
            StringTokenizer tok = new StringTokenizer(_customOptions);
            while (tok.hasMoreTokens()) {
                String pair = tok.nextToken();
                int eq = pair.indexOf('=');
                if ( (eq <= 0) || (eq >= pair.length()) )
                    continue;
                String key = pair.substring(0, eq);
                if (_noShowSet.contains(key))
                    continue;
                // leave in for HTTP and Connect so it can get migrated to MD5
                // hide for SOCKS until migrated to MD5
                if ((!TunnelController.TYPE_HTTP_CLIENT.equals(_type)) &&
                    (!TunnelController.TYPE_CONNECT.equals(_type)) &&
                    _nonProxyNoShowSet.contains(key))
                    continue;
                String val = pair.substring(eq+1);
                config.setProperty(OPT + key, val);
            }
        }

        config.setProperty(TunnelController.PROP_START, _startOnLoad + "");

        if (_tunnelQuantity != null) {
            config.setProperty("option.inbound.quantity", _tunnelQuantity);
            config.setProperty("option.outbound.quantity", _tunnelQuantity);
        }
        if (_tunnelDepth != null) {
            config.setProperty("option.inbound.length", _tunnelDepth);
            config.setProperty("option.outbound.length", _tunnelDepth);
        }
        if (_tunnelVariance != null) {
            config.setProperty("option.inbound.lengthVariance", _tunnelVariance);
            config.setProperty("option.outbound.lengthVariance", _tunnelVariance);
        }
        if (_tunnelBackupQuantity != null) {
            config.setProperty("option.inbound.backupQuantity", _tunnelBackupQuantity);
            config.setProperty("option.outbound.backupQuantity", _tunnelBackupQuantity);
        }
        if (_connectDelay)
            config.setProperty("option.i2p.streaming.connectDelay", "1000");
        else
            config.setProperty("option.i2p.streaming.connectDelay", "0");
        if (isClient(_type) && _sharedClient) {
            config.setProperty("option.inbound.nickname", CLIENT_NICKNAME);
            config.setProperty("option.outbound.nickname", CLIENT_NICKNAME);
        } else if (_name != null) {
            config.setProperty("option.inbound.nickname", _name);
            config.setProperty("option.outbound.nickname", _name);
        }
        if ("interactive".equals(_profile))
            // This was 1 which doesn't make much sense
            // The real way to make it interactive is to make the streaming lib
            // MessageInputStream flush faster but there's no option for that yet,
            // Setting it to 16 instead of the default but not sure what good that is either.
            config.setProperty("option.i2p.streaming.maxWindowSize", "16");
        else
            config.remove("option.i2p.streaming.maxWindowSize");
    }

    ///
    ///
    ///
    
    protected TunnelController getController(int tunnel) {
        if (tunnel < 0) return null;
        if (_group == null) return null;
        List<TunnelController> controllers = _group.getControllers();
        if (controllers.size() > tunnel)
            return controllers.get(tunnel); 
        else
            return null;
    }
    
    private static String getMessages(List<String> msgs) {
        StringBuilder buf = new StringBuilder(128);
        getMessages(msgs, buf);
        return buf.toString();
    }

    private static void getMessages(List<String> msgs, StringBuilder buf) {
        if (msgs == null) return;
        for (int i = 0; i < msgs.size(); i++) {
            buf.append(msgs.get(i)).append("\n");
        }
    }

    protected String _(String key) {
        return Messages._(key, _context);
    }

    /** translate (ngettext)
     *  @since 0.9.7
     */
    protected String ngettext(String s, String p, int n) {
        return Messages.ngettext(s, p, n, _context);
    }
}
