/* I2PTunnel is GPL'ed (with the exception mentioned in I2PTunnel.java)
 * (c) 2003 - 2004 mihi
 */
package net.i2p.i2ptunnel;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ConnectException;
import java.net.InetAddress;
import java.net.Inet4Address;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.security.GeneralSecurityException;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicLong;

import net.i2p.I2PException;
import net.i2p.I2PAppContext;
import net.i2p.client.I2PClient;
import net.i2p.client.I2PSession;
import net.i2p.client.I2PSessionException;
import net.i2p.client.streaming.I2PServerSocket;
import net.i2p.client.streaming.I2PSocket;
import net.i2p.client.streaming.I2PSocketManager;
import net.i2p.client.streaming.I2PSocketManagerFactory;
import net.i2p.client.streaming.RouterRestartException;
import net.i2p.client.streaming.IncomingConnectionFilter;
import net.i2p.client.streaming.StatefulConnectionFilter;
import net.i2p.crypto.SigType;
import net.i2p.data.Base64;
import net.i2p.data.DataHelper;
import net.i2p.data.Hash;
import net.i2p.util.EventDispatcher;
import net.i2p.util.I2PAppThread;
import net.i2p.util.I2PSSLSocketFactory;
import net.i2p.util.Log;
import net.i2p.i2ptunnel.access.FilterFactory;
import net.i2p.i2ptunnel.access.InvalidDefinitionException;

public class I2PTunnelServer extends I2PTunnelTask implements Runnable {

    protected final Log _log;
    protected final I2PSocketManager sockMgr;
    protected volatile I2PServerSocket i2pss;

    private final Object lock = new Object();
    protected final Object slock = new Object();
    protected final Object sslLock = new Object();

    protected InetAddress remoteHost;
    protected int remotePort;
    private final boolean _usePool;
    protected final Logging l;
    private I2PSSLSocketFactory _sslFactory;

    private static final long DEFAULT_READ_TIMEOUT = -1;
    /** default timeout - override if desired */
    protected long readTimeout = DEFAULT_READ_TIMEOUT;

    /** do we use threads? default true (ignored for standard servers, always false) */
    private static final String PROP_USE_POOL = "i2ptunnel.usePool";
    private static final boolean DEFAULT_USE_POOL = true;
    public static final String PROP_USE_SSL = "useSSL";
    public static final String PROP_UNIQUE_LOCAL = "enableUniqueLocal";
    /** @since 0.9.30 */
    public static final String PROP_ALT_PKF = "altPrivKeyFile";
    /** apparently unused */
    protected static volatile long __serverId = 0;
    /** max number of threads  - this many slowlorisses will DOS this server, but too high could OOM the JVM */
    private static final String PROP_HANDLER_COUNT = "i2ptunnel.blockingHandlerCount";
    private static final int DEFAULT_HANDLER_COUNT = 65;
    /** min number of threads */
    private static final int MIN_HANDLERS = 0;
    /** how long to wait before dropping an idle thread */
    private static final long HANDLER_KEEPALIVE_MS = 30*1000;

    protected I2PTunnelTask task;
    protected boolean bidir;
    private ThreadPoolExecutor _executor;
    protected volatile ThreadPoolExecutor _clientExecutor;
    private final Map<Integer, InetSocketAddress> _socketMap = new ConcurrentHashMap<Integer, InetSocketAddress>(4);

    /** unused? port should always be specified */
    private int DEFAULT_LOCALPORT = 4488;
    protected int localPort = DEFAULT_LOCALPORT;

    private volatile StatefulConnectionFilter _filter;

    /**
     *  Non-blocking
     *
     * @param privData Base64-encoded private key data,
     *                 format is specified in {@link net.i2p.data.PrivateKeyFile PrivateKeyFile}
     * @throws IllegalArgumentException if the I2CP configuration is b0rked so
     *                                  badly that we cant create a socketManager
     */
    public I2PTunnelServer(InetAddress host, int port, String privData, Logging l, EventDispatcher notifyThis, I2PTunnel tunnel) {
        super("Server at " + host + ':' + port, notifyThis, tunnel);
        _log = tunnel.getContext().logManager().getLog(getClass());
        ByteArrayInputStream bais = new ByteArrayInputStream(Base64.decode(privData));
        this.l = l;
        this.remoteHost = host;
        this.remotePort = port;
        _usePool = getUsePool();
        buildSocketMap(tunnel.getClientOptions());
        sockMgr = createManager(bais);
    }

    /**
     *  Non-blocking
     *
     * @param privkey file containing the private key data,
     *                format is specified in {@link net.i2p.data.PrivateKeyFile PrivateKeyFile}
     * @param privkeyname the name of the privKey file, just for logging
     * @throws IllegalArgumentException if the I2CP configuration is b0rked so
     *                                  badly that we cant create a socketManager
     */
    public I2PTunnelServer(InetAddress host, int port, File privkey, String privkeyname, Logging l,
                           EventDispatcher notifyThis, I2PTunnel tunnel) {
        super("Server at " + host + ':' + port, notifyThis, tunnel);
        _log = tunnel.getContext().logManager().getLog(getClass());
        this.l = l;
        this.remoteHost = host;
        this.remotePort = port;
        _usePool = getUsePool();
        buildSocketMap(tunnel.getClientOptions());
        FileInputStream fis = null;
        try {
            fis = new FileInputStream(privkey);
            sockMgr = createManager(fis);
        } catch (IOException ioe) {
            _log.error("Cannot read private key data for " + privkeyname, ioe);
            notifyEvent("openServerResult", "error");
            throw new IllegalArgumentException("Error starting server", ioe);
        } finally {
            if (fis != null)
                try { fis.close(); } catch (IOException ioe) {}
        }
    }

    /**
     *  Non-blocking
     *
     * @param privData stream containing the private key data,
     *                 format is specified in {@link net.i2p.data.PrivateKeyFile PrivateKeyFile}
     * @param privkeyname the name of the privKey file, just for logging
     * @throws IllegalArgumentException if the I2CP configuration is b0rked so
     *                                  badly that we cant create a socketManager
     */
    public I2PTunnelServer(InetAddress host, int port, InputStream privData, String privkeyname, Logging l,  EventDispatcher notifyThis, I2PTunnel tunnel) {
        super("Server at " + host + ':' + port, notifyThis, tunnel);
        _log = tunnel.getContext().logManager().getLog(getClass());
        this.l = l;
        this.remoteHost = host;
        this.remotePort = port;
        _usePool = getUsePool();
        buildSocketMap(tunnel.getClientOptions());
        sockMgr = createManager(privData);
    }

    /**
     *  Non-blocking
     *
     *  @param sktMgr the existing socket manager
     *  @since 0.8.9
     */
    public I2PTunnelServer(InetAddress host, int port, I2PSocketManager sktMgr,
                           Logging l, EventDispatcher notifyThis, I2PTunnel tunnel) {
        super("Server at " + host + ':' + port, notifyThis, tunnel);
        this.l = l;
        this.remoteHost = host;
        this.remotePort = port;
        _log = tunnel.getContext().logManager().getLog(getClass());
        _usePool = false;
        buildSocketMap(tunnel.getClientOptions());
        sockMgr = sktMgr;
        open = true;
    }

    /** @since 0.9.8 */
    private boolean getUsePool() {
        // extending classes default to threaded, but for a standard server, we can't get slowlorissed
        boolean rv = !getClass().equals(I2PTunnelServer.class);
        if (rv) {
            String usePool = getTunnel().getClientOptions().getProperty(PROP_USE_POOL);
            if (usePool != null)
                rv = Boolean.parseBoolean(usePool);
            else
                rv = DEFAULT_USE_POOL;
        }
        return rv;
    }

    private static final int RETRY_DELAY = 20*1000;
    private static final int MAX_RETRIES = 4;

    /**
     *
     * @throws IllegalArgumentException if the I2CP configuration is b0rked so
     *                                  badly that we cant create a socketManager
     * @since 0.9.8
     */
    private I2PSocketManager createManager(InputStream privData) {
        Properties props = new Properties();
        props.putAll(getTunnel().getClientOptions());
        int portNum = I2PClient.DEFAULT_LISTEN_PORT;
        if (getTunnel().port != null) {
            try {
                portNum = Integer.parseInt(getTunnel().port);
            } catch (NumberFormatException nfe) {
                _log.error("Invalid port specified [" + getTunnel().port + "], reverting to " + portNum);
            }
        }

        if (getTunnel().filterDefinition != null) {
            File filterDefinition = new File(getTunnel().filterDefinition);
            I2PAppContext context = getTunnel().getContext();
            try {
                _filter = FilterFactory.createFilter(context, filterDefinition);
            } catch (IOException | InvalidDefinitionException bad) {
                throw new IllegalArgumentException("Bad filter definition file: " + bad.getMessage(), bad);
            }
        }

        IncomingConnectionFilter filter = _filter == null ? IncomingConnectionFilter.ALLOW : _filter;

        try {
            I2PSocketManager rv = I2PSocketManagerFactory.createDisconnectedManager(privData, getTunnel().host,
                                                                                    portNum, props, filter);
            rv.setName("I2PTunnel Server");
            getTunnel().addSession(rv.getSession());
            String alt = props.getProperty(PROP_ALT_PKF);
            if (alt != null)
                addSubsession(rv, alt);
            return rv;
        } catch (I2PSessionException ise) {
            throw new IllegalArgumentException("Can't create socket manager", ise);
        } finally {
            try { privData.close(); } catch (IOException ioe) {}
        }
    }

    /**
     *  Add a non-DSA_SHA1 subsession to the DSA_SHA1 server if necessary.
     *
     *  @return subsession, or null if none was added
     *  @since 0.9.30
     */
    private I2PSession addSubsession(I2PSocketManager sMgr, String alt) {
        File altFile = TunnelController.filenameToFile(alt);
        if (altFile == null)
            return null;
        I2PSession sess = sMgr.getSession();
        if (sess.getMyDestination().getSigType() != SigType.DSA_SHA1)
            return null;
        Properties props = new Properties();
        props.putAll(getTunnel().getClientOptions());
        // fixme get actual sig type
        String name = props.getProperty("inbound.nickname");
        if (name != null)
            props.setProperty("inbound.nickname", name + " (EdDSA)");
        name = props.getProperty("outbound.nickname");
        if (name != null)
            props.setProperty("outbound.nickname", name + " (EdDSA)");
        props.setProperty(I2PClient.PROP_SIGTYPE, "EdDSA_SHA512_Ed25519");
        FileInputStream privData = null;
        try {
            privData = new FileInputStream(altFile);
            I2PSession rv = sMgr.addSubsession(privData, props);
            if (rv.isOffline()) {
                long exp = rv.getOfflineExpiration();
                long remaining = exp - getTunnel().getContext().clock().now();
                // if expires before the LS expires...
                if (remaining <= 10*60*1000) {
                    String msg;
                    if (remaining > 0)
                        msg = "Offline signature for tunnel " + name + " alternate destination expires in " + DataHelper.formatTime(exp);
                    else
                        msg = "Offline signature for tunnel " + name + " alternate destination expired " + DataHelper.formatTime(exp);
                    _log.log(Log.CRIT, msg);
                    throw new IllegalArgumentException(msg);
                }
                if (remaining < 60*24*60*60*1000L) {
                    String msg = "Offline signature for tunnel " + name + " alternate destination expires in " + DataHelper.formatDuration(remaining);
                    _log.logAlways(Log.WARN, msg);
                    l.log("WARNING: " + msg);
                }
            }
            return rv;
        } catch (IOException ioe) {
            _log.error("Failed to add subssession", ioe);
            return null;
        } catch (I2PSessionException ise) {
            _log.error("Failed to add subssession", ise);
            return null;
        } finally {
            if (privData != null) try { privData.close(); } catch (IOException ioe) {}
        }
    }


    /**
     * Warning, blocks while connecting to router and building tunnels;
     *
     * @throws IllegalArgumentException if the I2CP configuration is b0rked so
     *                                  badly that we cant create a socketManager
     * @since 0.9.8
     */
    private void connectManager() {
        int retries = 0;
        I2PSession session = sockMgr.getSession();
        if (session.isOffline()) {
            long exp = session.getOfflineExpiration();
            long remaining = exp - getTunnel().getContext().clock().now();
            Properties props = getTunnel().getClientOptions();
            String name = props.getProperty("inbound.nickname");
            if (name == null) {
                name = props.getProperty("outbound.nickname");
                if (name == null)
                    name = "";
            }
            // if expires before the LS expires...
            if (remaining <= 10*60*1000) {
                String msg;
                if (remaining > 0)
                    msg = "Offline signature for tunnel " + name + " expires in " + DataHelper.formatTime(exp);
                else
                    msg = "Offline signature for tunnel " + name + " expired " + DataHelper.formatTime(exp);
                _log.log(Log.CRIT, msg);
                throw new IllegalArgumentException(msg);
            }
            if (remaining < 60*24*60*60*1000L) {
                String msg = "Offline signature for tunnel " + name + " expires in " + DataHelper.formatDuration(remaining);
                _log.logAlways(Log.WARN, msg);
                l.log("WARNING: " + msg);
            }
        }
        while (session.isClosed()) {
            try {
                session.connect();
                // Now connect the subsessions, if any
                List<I2PSession> subs = sockMgr.getSubsessions();
                if (!subs.isEmpty()) {
                    for (I2PSession sub : subs) {
                        try {
                            sub.connect();
                            if (_log.shouldInfo())
                                _log.info("Connected subsession " + sub);
                        } catch (I2PSessionException ise) {
                            // not fatal?
                            String msg = "Unable to connect subsession " + sub;
                            this.l.log(msg);
                            _log.error(msg, ise);
                        }
                    }
                }
            } catch (I2PSessionException ise) {
                // try to make this error sensible as it will happen...
                String portNum = getTunnel().port;
                if (portNum == null)
                    portNum = Integer.toString(I2PClient.DEFAULT_LISTEN_PORT);
                String msg;
                if (getTunnel().getContext().isRouterContext())
                    msg = "Unable to build tunnels for the server at " + remoteHost.getHostAddress() + ':' + remotePort;
                else
                    msg = "Unable to connect to the router at " + getTunnel().host + ':' + portNum +
                             " and build tunnels for the server at " + remoteHost.getHostAddress() + ':' + remotePort;
                String exmsg = ise.getMessage();
                boolean fail = exmsg != null && exmsg.contains("session limit exceeded");
                if (!fail && ++retries < MAX_RETRIES) {
                    msg += ", retrying in " + (RETRY_DELAY / 1000) + " seconds";
                    this.l.log(msg);
                    _log.error(msg);
                } else {
                    msg += ", giving up";
                    this.l.log(msg);
                    _log.log(Log.CRIT, msg, ise);
                    throw new IllegalArgumentException(msg, ise);
                }
                try { Thread.sleep(RETRY_DELAY); } catch (InterruptedException ie) {}
            }
        }

        l.log("Tunnels ready for server at " + remoteHost.getHostAddress() + ':' + remotePort);
        notifyEvent("openServerResult", "ok");
        open = true;
    }

    /**
     *  Copy input stream to a byte array, so we can retry
     *  @since 0.7.10
     */
/****
    private static ByteArrayInputStream copyOfInputStream(InputStream is) throws IOException {
        byte[] buf = new byte[128];
        ByteArrayOutputStream os = new ByteArrayOutputStream(768);
        try {
            int read;
            while ((read = is.read(buf)) >= 0) {
                os.write(buf, 0, read);
            }
        } finally {
             try { is.close(); } catch (IOException ioe) {}
             // don't need to close BAOS
        }
        return new ByteArrayInputStream(os.toByteArray());
    }
****/
    
    /**
     * Start running the I2PTunnelServer.
     * Warning, blocks while connecting to router and building tunnels;
     *
     * @throws IllegalArgumentException if the I2CP configuration is b0rked so
     *                                  badly that we cant create a socketManager
     */
    public synchronized void startRunning() {
        connectManager();
        StatefulConnectionFilter filter = _filter;
        if (filter != null)
            filter.start();
        // prevent JVM exit when running outside the router
        boolean isDaemon = getTunnel().getContext().isRouterContext();
        Thread t = new I2PAppThread(this, "Server " + remoteHost + ':' + remotePort, isDaemon);
        t.start();
    }

    /**
     * Set the read idle timeout for newly-created connections (in
     * milliseconds).  After this time expires without data being reached from
     * the I2P network, the connection itself will be closed.
     *
     * Less than or equal to 0 means forever.
     * Default -1 (forever) as of 0.9.36 for standard tunnels,
     * but extending classes may override.
     * Prior to that, default was 5 minutes, but did not work
     * due to streaming bugs.
     *
     * Applies only to future connections;
     * calling this does not affect existing connections.
     *
     * @param ms in ms
     */
    public void setReadTimeout(long ms) {
        readTimeout = ms;
    }
    
    /**
     * Get the read idle timeout for newly-created connections (in
     * milliseconds).
     *
     * Less than or equal to 0 means forever.
     * Default -1 (forever) as of 0.9.36 for standard tunnels,
     * but extending classes may override.
     * Prior to that, default was 5 minutes, but did not work
     * due to streaming bugs.
     *
     * @return The read timeout used for connections
     */
    public long getReadTimeout() {
        return readTimeout;
    }

    /**
     *  Note that the tunnel can be reopened after this by calling startRunning().
     *  This does not release all resources. In particular, the I2PSocketManager remains
     *  and it may have timer threads that continue running.
     *
     *  To release all resources permanently, call destroy().
     */
    public synchronized boolean close(boolean forced) {
        if (!open) return true;
        if (task != null) {
            task.close(forced);
        }
        synchronized (lock) {
            if (!forced && sockMgr.listSockets().size() != 0) {
                l.log("There are still active connections!");
                for (I2PSocket skt : sockMgr.listSockets()) {
                    l.log("->" + skt);
                }
                return false;
            }
            l.log("Stopping tunnels for server at " + this.remoteHost + ':' + this.remotePort);
            open = false;
            try {
                if (i2pss != null) {
                    i2pss.close();
                    i2pss = null;
                }
                I2PSession session = sockMgr.getSession();
                getTunnel().removeSession(session);
                session.destroySession();
            } catch (I2PException ex) {
                _log.error("Error destroying the session", ex);
                //System.exit(1);
            }
            //l.log("Server shut down.");
            if (_usePool && _executor != null) {
                _executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
                _executor.shutdownNow();
            }
            return true;
        }
    }

    /**
     *  Note that the tunnel cannot be reopened after this by calling startRunning(),
     *  as it will destroy the underlying socket manager.
     *  This releases all resources.
     *
     *  @since 0.9.17
     */
    @Override
    public synchronized boolean destroy() {
        close(true);
        sockMgr.destroySocketManager();
        StatefulConnectionFilter filter = _filter;
        if (filter != null)
            filter.stop();
        return true;
    }

    /**
     *  Update the I2PSocketManager.
     *  And since 0.9.15, the target host and port.
     *
     *  @since 0.9.1
     */
    @Override
    public void optionsUpdated(I2PTunnel tunnel) {
        if (getTunnel() != tunnel || sockMgr == null)
            return;
        Properties props = tunnel.getClientOptions();
        sockMgr.setDefaultOptions(sockMgr.buildOptions(props));
        // see TunnelController.setSessionOptions()
        String h = props.getProperty(TunnelController.PROP_TARGET_HOST);
        if (h != null) {
            try {
                remoteHost = InetAddress.getByName(h);
            } catch (UnknownHostException uhe) {
                l.log("Unknown host: " + h);
            }
        }
        String p = props.getProperty(TunnelController.PROP_TARGET_PORT);
        if (p != null) {
            try {
                int port = Integer.parseInt(p);
                if (port > 0 && port <= 65535)
                    remotePort = port;
                else
                    l.log("Bad port: " + port);
            } catch (NumberFormatException nfe) {
                l.log("Bad port: " + p);
            }
        }
        buildSocketMap(props);
    }

    /**
     *  Update the ports map.
     *
     *  @since 0.9.9
     */
    private void buildSocketMap(Properties props) {
        _socketMap.clear();
        for (Map.Entry<Object, Object> e : props.entrySet()) {
            String key = (String) e.getKey();
            if (key.startsWith("targetForPort.")) {
                key = key.substring("targetForPort.".length());
                try {
                    int myPort = Integer.parseInt(key);
                    String host = (String) e.getValue();
                    int colon = host.indexOf(':');
                    int port = Integer.parseInt(host.substring(colon + 1));
                    host = host.substring(0, colon);
                    InetSocketAddress isa = new InetSocketAddress(host, port);
                    if (isa.isUnresolved())
                        l.log("Warning - cannot resolve address for port " + key + ": " + host);
                    _socketMap.put(Integer.valueOf(myPort), isa);
                } catch (NumberFormatException nfe) {
                    l.log("Bad socket spec for port " + key + ": " + e.getValue());
                } catch (IndexOutOfBoundsException ioobe) {
                    l.log("Bad socket spec for port " + key + ": " + e.getValue());
                }
            }
        }
    }

    protected int getHandlerCount() { 
        int rv = DEFAULT_HANDLER_COUNT;
        String cnt = getTunnel().getClientOptions().getProperty(PROP_HANDLER_COUNT);
        if (cnt != null) {
            try {
                rv = Integer.parseInt(cnt);
                if (rv <= 0)
                    rv = DEFAULT_HANDLER_COUNT;
            } catch (NumberFormatException nfe) {}
        }
        return rv;
    }
    
    /**
     *  If usePool is set, this starts the executor pool.
     *  Then, do the accept() loop, and either
     *  hands each I2P socket to the executor or runs it in-line.
     */
    public void run() {
        i2pss = sockMgr.getServerSocket();
        if (_log.shouldLog(Log.WARN)) {
            if (_usePool)
                _log.warn("Starting executor with " + getHandlerCount() + " threads max");
            else
                _log.warn("Threads disabled, running blockingHandles inline");
        }
        if (_usePool) {
            _executor = new CustomThreadPoolExecutor(getHandlerCount(), "ServerHandler pool " + remoteHost + ':' + remotePort);
        }
        TunnelControllerGroup tcg = TunnelControllerGroup.getInstance();
        if (tcg != null) {
            _clientExecutor = tcg.getClientExecutor();
        } else {
            // Fallback in case TCG.getInstance() is null, never instantiated
            // and we were not started by TCG.
            // Maybe a plugin loaded before TCG? Should be rare.
            // Never shut down.
            _clientExecutor = new TunnelControllerGroup.CustomThreadPoolExecutor();
        }
        I2PSocket i2ps = null;
        while (open) {
            try {
                i2ps = null;
                I2PServerSocket ci2pss = i2pss;
                if (ci2pss == null)
                    throw new I2PException("I2PServerSocket closed");
                // returns non-null as of 0.9.17
                i2ps = ci2pss.accept();
                if (_usePool) {
                    try {
                        _executor.execute(new Handler(i2ps));
                    } catch (RejectedExecutionException ree) {
                         try {
                             i2ps.reset();
                         } catch (IOException ioe) {}
                         if (open)
                             _log.logAlways(Log.WARN, "ServerHandler queue full, dropping incoming connection to " +
                                        remoteHost + ':' + remotePort +
                                        "; increase server max threads or " + PROP_HANDLER_COUNT +
                                        "; current is " + getHandlerCount());
                    }
                } else {
                    // use only for standard servers that can't get slowlorissed! Not for http or irc
                    blockingHandle(i2ps);
                }
            } catch (RouterRestartException rre) {
                // Delay and loop if router is soft restarting
                _log.logAlways(Log.WARN, "Waiting for router restart");
                if (i2ps != null) try { i2ps.close(); } catch (IOException ioe) {}
                try {
                    Thread.sleep(2*60*1000);
                } catch (InterruptedException ie) {}
                // This should be the same as before, but we have to call getServerSocket()
                // so sockMgr will call ConnectionManager.setAllowIncomingConnections(true) again
                _log.logAlways(Log.WARN, "Reconnecting to router after restart");
                i2pss = sockMgr.getServerSocket();
            } catch (I2PException ipe) {
                String s = "Error accepting - KILLING THE TUNNEL SERVER";
                _log.log(Log.CRIT, s, ipe);
                l.log(s + ": " + ipe);
                // Tell TunnelController so it will change state
                TunnelController tc = getTunnel().getController();
                if (tc != null)
                    tc.stopTunnel();
                else
                    close(true);
                if (i2ps != null) try { i2ps.close(); } catch (IOException ioe) {}
                break;
            } catch (ConnectException ce) {
                if (i2ps != null) try { i2ps.close(); } catch (IOException ioe) {}
                if (!open)
                    break;
                if (_log.shouldLog(Log.ERROR))
                    _log.error("Error accepting", ce);
                try {
                    Thread.sleep(2*60*1000);
                } catch (InterruptedException ie) {}
                // Server socket possbily closed out from under us, perhaps as part of a router restart;
                // wait a while and try to get a new socket
                i2pss = sockMgr.getServerSocket();
            } catch(SocketTimeoutException ste) {
                // ignored, we never set the timeout
                if (i2ps != null) try { i2ps.close(); } catch (IOException ioe) {}
            } catch (RuntimeException e) {
                // streaming borkage
                if (_log.shouldLog(Log.ERROR))
                    _log.error("Uncaught exception accepting", e);
                if (i2ps != null) try { i2ps.close(); } catch (IOException ioe) {}
                // not killing the server..
                try {
                    Thread.sleep(500);
                } catch (InterruptedException ie) {}
            }
        }
        if (_executor != null && !_executor.isTerminating() && !_executor.isShutdown())
            _executor.shutdownNow();
    }
    
    /**
     * Not really needed for now but in case we want to add some hooks like afterExecute().
     */
    private static class CustomThreadPoolExecutor extends ThreadPoolExecutor {
        public CustomThreadPoolExecutor(int max, String name) {
             super(MIN_HANDLERS, max, HANDLER_KEEPALIVE_MS, TimeUnit.MILLISECONDS,
                   new SynchronousQueue<Runnable>(), new CustomThreadFactory(name));
        }
    }

    /** just to set the name and set Daemon */
    private static class CustomThreadFactory implements ThreadFactory {
        private final String _name;
        private final AtomicLong _executorThreadCount = new AtomicLong();

        public CustomThreadFactory(String name) {
            _name = name;
        }

        public Thread newThread(Runnable r) {
            Thread rv = Executors.defaultThreadFactory().newThread(r);
            rv.setName(_name + ' ' + _executorThreadCount.incrementAndGet());
            rv.setDaemon(true);
            return rv;
        }
    }

    public boolean shouldUsePool() { return _usePool; }
    
    /**
     * Run the blockingHandler.
     */
    private class Handler implements Runnable { 
        private final I2PSocket _i2ps;

        public Handler(I2PSocket socket) {
            _i2ps = socket;
        }

        public void run() {
            try {
                blockingHandle(_i2ps);   
            } catch (Throwable t) {
                _log.error("Uncaught error in i2ptunnel server", t);
            }
        }
    }
    
    /**
     *  This is run in a thread from a limited-size thread pool via Handler.run(),
     *  except for a standard server (this class, no extension, as determined in getUsePool()),
     *  it is run directly in the acceptor thread (see run()).
     *
     *  In either case, this method and any overrides must spawn a thread and return quickly.
     *  If blocking while reading the headers (as in HTTP and IRC), the thread pool
     *  may be exhausted.
     *
     *  See PROP_USE_POOL, DEFAULT_USE_POOL, PROP_HANDLER_COUNT, DEFAULT_HANDLER_COUNT
     */
    protected void blockingHandle(I2PSocket socket) {
        if (_log.shouldLog(Log.INFO))
            _log.info("Incoming connection to '" + toString() + "' port " + socket.getLocalPort() +
                      " from: " + socket.getPeerDestination().calculateHash() + " port " + socket.getPort());
        long afterAccept = getTunnel().getContext().clock().now();
        long afterSocket = -1;
        //local is fast, so synchronously. Does not need that many
        //threads.
        try {
            socket.setReadTimeout(readTimeout);
            Socket s = getSocket(socket.getPeerDestination().calculateHash(), socket.getLocalPort());
            afterSocket = getTunnel().getContext().clock().now();
            Thread t = new I2PTunnelRunner(s, socket, slock, null, null,
                                           null, (I2PTunnelRunner.FailCallback) null);
            // run in the unlimited client pool
            //t.start();
            _clientExecutor.execute(t);

            long afterHandle = getTunnel().getContext().clock().now();
            long timeToHandle = afterHandle - afterAccept;
            if ( (timeToHandle > 1000) && (_log.shouldLog(Log.WARN)) )
                _log.warn("Took a while to handle the request for " + remoteHost + ':' + remotePort +
                          " [" + timeToHandle + ", socket create: " + (afterSocket-afterAccept) + "]");
        } catch (SocketException ex) {
            try {
                socket.reset();
            } catch (IOException ioe) {}
            if (_log.shouldLog(Log.ERROR))
                _log.error("Error connecting to server " + remoteHost + ':' + remotePort, ex);
        } catch (IOException ex) {
            _log.error("Error while waiting for I2PConnections", ex);
        }
    }

    /**
     *  Get a regular or SSL socket depending on config and the incoming port.
     *  To configure a specific host:port as the server for incoming port xx,
     *  set option targetForPort.xx=host:port
     *
     *  @param from may be used to construct local address since 0.9.13
     *  @since 0.9.9
     */
    protected Socket getSocket(Hash from, int incomingPort) throws IOException {
        InetAddress host = remoteHost;
        int port = remotePort;
        if (incomingPort != 0 && !_socketMap.isEmpty()) {
            InetSocketAddress isa = _socketMap.get(Integer.valueOf(incomingPort));
            if (isa != null) {
                host = isa.getAddress();
                if (host == null)
                    throw new IOException("Cannot resolve " + isa.getHostName());
                port = isa.getPort();
            }
        }
        // don't do SSL-over-SSL
        boolean force = incomingPort == 443 || incomingPort == 22;
        return getSocket(from, host, port, force);
    }

    /**
     *  Get a regular or SSL socket depending on config.
     *  The SSL config applies to all hosts/ports.
     *
     *  @param from may be used to construct local address since 0.9.13
     *  @since 0.9.9
     */
    protected Socket getSocket(Hash from, InetAddress remoteHost, int remotePort) throws IOException {
        return getSocket(from, remoteHost, remotePort, false);
    }

    /**
     *  Get a regular or SSL socket depending on config.
     *  The SSL config applies to all hosts/ports, unless forced off.
     *
     *  @param forceNonSSL override config
     *  @since 0.9.50
     */
    private Socket getSocket(Hash from, InetAddress remoteHost, int remotePort, boolean forceNonSSL) throws IOException {
        String opt = getTunnel().getClientOptions().getProperty(PROP_USE_SSL);
        if (!forceNonSSL && Boolean.parseBoolean(opt)) {
            synchronized(sslLock) {
                if (_sslFactory == null) {
                    try {
                        _sslFactory = new I2PSSLSocketFactory(getTunnel().getContext(),
                                                               true, "certificates/i2ptunnel");
                    } catch (GeneralSecurityException gse) {
                        IOException ioe = new IOException("SSL Fail");
                        ioe.initCause(gse);
                        throw ioe;
                    }
                }
            }
            return _sslFactory.createSocket(remoteHost, remotePort);
        } else {
            // as suggested in https://lists.torproject.org/pipermail/tor-dev/2014-March/006576.html
            boolean unique = Boolean.parseBoolean(getTunnel().getClientOptions().getProperty(PROP_UNIQUE_LOCAL));
            if (unique && remoteHost.isLoopbackAddress()) {
                byte[] addr;
                if (remoteHost instanceof Inet4Address) {
                    addr = new byte[4];
                    addr[0] = 127;
                    System.arraycopy(from.getData(), 0, addr, 1, 3);
                } else {
                    addr = new byte[16];
                    addr[0] = (byte) 0xfd;
                    System.arraycopy(from.getData(), 0, addr, 1, 15);
                }
                InetAddress local = InetAddress.getByAddress(addr);
                // Javadocs say local port of 0 allowed in Java 7.
                // Not clear if supported in Java 6 or not.
                return new Socket(remoteHost, remotePort, local, 0);
            } else {
                return new Socket(remoteHost, remotePort);
            }
        }
    }
}

