package org.littleshoot.proxy.impl;

import org.littleshoot.proxy.ActivityTracker;
import org.littleshoot.proxy.ChainedProxyManager;
import org.littleshoot.proxy.DefaultHostResolver;
import org.littleshoot.proxy.DnsSecServerResolver;
import org.littleshoot.proxy.HostResolver;
import org.littleshoot.proxy.HttpFilters;
import org.littleshoot.proxy.HttpFiltersSource;
import org.littleshoot.proxy.HttpFiltersSourceAdapter;
import org.littleshoot.proxy.HttpProxyServer;
import org.littleshoot.proxy.HttpProxyServerBootstrap;
import org.littleshoot.proxy.MitmManager;
import org.littleshoot.proxy.ProxyAuthenticator;
import org.littleshoot.proxy.SslEngineSource;
import org.littleshoot.proxy.TransportProtocol;
import org.littleshoot.proxy.UnknownTransportProtocolException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.util.Collection;
import java.util.Properties;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.net.ssl.SSLEngine;

import io.netty.bootstrap.ChannelFactory;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.ServerChannel;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.ChannelGroupFuture;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.traffic.GlobalTrafficShapingHandler;
import io.netty.util.concurrent.GlobalEventExecutor;

/**
 * <p>
 * Primary implementation of an {@link HttpProxyServer}.
 * </p>
 * <p>
 * <p>
 * {@link DefaultHttpProxyServer} is bootstrapped by calling
 * {@link #bootstrap()} or {@link #bootstrapFromFile(String)}, and then calling
 * {@link DefaultHttpProxyServerBootstrap#start()}. For example:
 * </p>
 * <p>
 * <pre>
 * DefaultHttpProxyServer server =
 *         DefaultHttpProxyServer
 *                 .bootstrap()
 *                 .withPort(8090)
 *                 .start();
 * </pre>
 */
public class DefaultHttpProxyServer implements HttpProxyServer {
    private static final Logger LOG = LoggerFactory.getLogger(DefaultHttpProxyServer.class);

    /**
     * The interval in ms at which the GlobalTrafficShapingHandler will run to compute and throttle the
     * proxy-to-server bandwidth.
     */
    private static final long TRAFFIC_SHAPING_CHECK_INTERVAL_MS = 250L;

    private static final int MAX_INITIAL_LINE_LENGTH_DEFAULT = 8192;
    private static final int MAX_HEADER_SIZE_DEFAULT = 8192 * 2;
    private static final int MAX_CHUNK_SIZE_DEFAULT = 8192 * 2;

    /**
     * The proxy alias to use in the Via header if no explicit proxy alias is specified and the hostname of the local
     * machine cannot be resolved.
     */
    private static final String FALLBACK_PROXY_ALIAS = "littleproxy";

    /**
     * Our {@link ServerGroup}. Multiple proxy servers can share the same
     * ServerGroup in order to reuse threads and other such resources.
     */
    private final ServerGroup serverGroup;

    private final TransportProtocol transportProtocol;
    /*
     * The address that the server will attempt to bind to.
     */
    private final InetSocketAddress requestedAddress;
    /*
     * The actual address to which the server is bound. May be different from the requestedAddress in some circumstances,
     * for example when the requested port is 0.
     */
    private volatile InetSocketAddress localAddress;
    private volatile InetSocketAddress boundAddress;
    private final SslEngineSource sslEngineSource;
    private final boolean authenticateSslClients;
    private final ProxyAuthenticator proxyAuthenticator;
    private final ChainedProxyManager chainProxyManager;
    private final MitmManager mitmManager;
    private final HttpFiltersSource filtersSource;
    private final boolean transparent;
    private volatile int connectTimeout;
    private volatile int idleConnectionTimeout;
    private final HostResolver serverResolver;
    private volatile GlobalTrafficShapingHandler globalTrafficShapingHandler;
    private final int maxInitialLineLength;
    private final int maxHeaderSize;
    private final int maxChunkSize;
    private final boolean allowRequestsToOriginServer;

    /**
     * The alias or pseudonym for this proxy, used when adding the Via header.
     */
    private final String proxyAlias;

    /**
     * True when the proxy has already been stopped by calling {@link #stop()} or {@link #abort()}.
     */
    private final AtomicBoolean stopped = new AtomicBoolean(false);

    /**
     * Track all ActivityTrackers for tracking proxying activity.
     */
    private final Collection<ActivityTracker> activityTrackers = new ConcurrentLinkedQueue<ActivityTracker>();

    /**
     * Keep track of all channels created by this proxy server for later shutdown when the proxy is stopped.
     */
    private final ChannelGroup allChannels = new DefaultChannelGroup("HTTP-Proxy-Server", GlobalEventExecutor.INSTANCE);

    /**
     * JVM shutdown hook to shutdown this proxy server. Declared as a class-level variable to allow removing the shutdown hook when the
     * proxy server is stopped normally.
     */
    private final Thread jvmShutdownHook = new Thread(new Runnable() {
        @Override
        public void run() {
            abort();
        }
    }, "LittleProxy-JVM-shutdown-hook");

    /**
     * Bootstrap a new {@link DefaultHttpProxyServer} starting from scratch.
     *
     * @return
     */
    public static HttpProxyServerBootstrap bootstrap() {
        return new DefaultHttpProxyServerBootstrap();
    }

    /**
     * Bootstrap a new {@link DefaultHttpProxyServer} using defaults from the
     * given file.
     *
     * @param path
     * @return
     */
    public static HttpProxyServerBootstrap bootstrapFromFile(String path) {
        final File propsFile = new File(path);
        Properties props = new Properties();

        if (propsFile.isFile()) {
            try (InputStream is = new FileInputStream(propsFile)) {
                props.load(is);
            } catch (final IOException e) {
                LOG.warn("Could not load props file?", e);
            }
        }

        return new DefaultHttpProxyServerBootstrap(props);
    }

    /**
     * Creates a new proxy server.
     *
     * @param serverGroup                 our ServerGroup for shared thread pools and such
     * @param transportProtocol           The protocol to use for data transport
     * @param requestedAddress            The address on which this server will listen
     * @param sslEngineSource             (optional) if specified, this Proxy will encrypt inbound
     *                                    connections from clients using an {@link SSLEngine} obtained
     *                                    from this {@link SslEngineSource}.
     * @param authenticateSslClients      Indicate whether or not to authenticate clients when using SSL
     * @param proxyAuthenticator          (optional) If specified, requests to the proxy will be
     *                                    authenticated using HTTP BASIC authentication per the provided
     *                                    {@link ProxyAuthenticator}
     * @param chainProxyManager           The proxy to send requests to if chaining proxies. Typically
     *                                    <code>null</code>.
     * @param mitmManager                 The {@link MitmManager} to use for man in the middle'ing
     *                                    CONNECT requests
     * @param filtersSource               Source for {@link HttpFilters}
     * @param transparent                 If true, this proxy will run as a transparent proxy. This will
     *                                    not modify the response, and will only modify the request to
     *                                    amend the URI if the target is the origin server (to comply
     *                                    with RFC 7230 section 5.3.1).
     * @param idleConnectionTimeout       The timeout (in seconds) for auto-closing idle connections.
     * @param activityTrackers            for tracking activity on this proxy
     * @param connectTimeout              number of milliseconds to wait to connect to the upstream
     *                                    server
     * @param serverResolver              the {@link HostResolver} to use for resolving server addresses
     * @param readThrottleBytesPerSecond  read throttle bandwidth
     * @param writeThrottleBytesPerSecond write throttle bandwidth
     * @param maxInitialLineLength
     * @param maxHeaderSize
     * @param maxChunkSize
     * @param allowRequestsToOriginServer when true, allow the proxy to handle requests that contain an origin-form URI, as defined in RFC 7230 5.3.1
     */
    private DefaultHttpProxyServer(ServerGroup serverGroup,
                                   TransportProtocol transportProtocol,
                                   InetSocketAddress requestedAddress,
                                   SslEngineSource sslEngineSource,
                                   boolean authenticateSslClients,
                                   ProxyAuthenticator proxyAuthenticator,
                                   ChainedProxyManager chainProxyManager,
                                   MitmManager mitmManager,
                                   HttpFiltersSource filtersSource,
                                   boolean transparent,
                                   int idleConnectionTimeout,
                                   Collection<ActivityTracker> activityTrackers,
                                   int connectTimeout,
                                   HostResolver serverResolver,
                                   long readThrottleBytesPerSecond,
                                   long writeThrottleBytesPerSecond,
                                   InetSocketAddress localAddress,
                                   String proxyAlias,
                                   int maxInitialLineLength,
                                   int maxHeaderSize,
                                   int maxChunkSize,
                                   boolean allowRequestsToOriginServer) {
        this.serverGroup = serverGroup;
        this.transportProtocol = transportProtocol;
        this.requestedAddress = requestedAddress;
        this.sslEngineSource = sslEngineSource;
        this.authenticateSslClients = authenticateSslClients;
        this.proxyAuthenticator = proxyAuthenticator;
        this.chainProxyManager = chainProxyManager;
        this.mitmManager = mitmManager;
        this.filtersSource = filtersSource;
        this.transparent = transparent;
        this.idleConnectionTimeout = idleConnectionTimeout;
        if (activityTrackers != null) {
            this.activityTrackers.addAll(activityTrackers);
        }
        this.connectTimeout = connectTimeout;
        this.serverResolver = serverResolver;

        if (writeThrottleBytesPerSecond > 0 || readThrottleBytesPerSecond > 0) {
            this.globalTrafficShapingHandler = createGlobalTrafficShapingHandler(transportProtocol, readThrottleBytesPerSecond, writeThrottleBytesPerSecond);
        } else {
            this.globalTrafficShapingHandler = null;
        }
        this.localAddress = localAddress;

        if (proxyAlias == null) {
            // attempt to resolve the name of the local machine. if it cannot be resolved, use the fallback name.
            String hostname = ProxyUtils.getHostName();
            if (hostname == null) {
                hostname = FALLBACK_PROXY_ALIAS;
            }
            this.proxyAlias = hostname;
        } else {
            this.proxyAlias = proxyAlias;
        }
        this.maxInitialLineLength = maxInitialLineLength;
        this.maxHeaderSize = maxHeaderSize;
        this.maxChunkSize = maxChunkSize;
        this.allowRequestsToOriginServer = allowRequestsToOriginServer;
    }

    /**
     * Creates a new GlobalTrafficShapingHandler for this HttpProxyServer, using this proxy's proxyToServerEventLoop.
     *
     * @param transportProtocol
     * @param readThrottleBytesPerSecond
     * @param writeThrottleBytesPerSecond
     * @return
     */
    private GlobalTrafficShapingHandler createGlobalTrafficShapingHandler(TransportProtocol transportProtocol, long readThrottleBytesPerSecond, long writeThrottleBytesPerSecond) {
        EventLoopGroup proxyToServerEventLoop = this.getProxyToServerWorkerFor(transportProtocol);
        return new GlobalTrafficShapingHandler(proxyToServerEventLoop,
                writeThrottleBytesPerSecond,
                readThrottleBytesPerSecond,
                TRAFFIC_SHAPING_CHECK_INTERVAL_MS,
                Long.MAX_VALUE);
    }

    boolean isTransparent() {
        return transparent;
    }

    @Override
    public int getIdleConnectionTimeout() {
        return idleConnectionTimeout;
    }

    @Override
    public void setIdleConnectionTimeout(int idleConnectionTimeout) {
        this.idleConnectionTimeout = idleConnectionTimeout;
    }

    @Override
    public int getConnectTimeout() {
        return connectTimeout;
    }

    @Override
    public void setConnectTimeout(int connectTimeoutMs) {
        this.connectTimeout = connectTimeoutMs;
    }

    public HostResolver getServerResolver() {
        return serverResolver;
    }

    public InetSocketAddress getLocalAddress() {
        return localAddress;
    }

    @Override
    public InetSocketAddress getListenAddress() {
        return boundAddress;
    }

    @Override
    public void setThrottle(long readThrottleBytesPerSecond, long writeThrottleBytesPerSecond) {
        if (globalTrafficShapingHandler != null) {
            globalTrafficShapingHandler.configure(writeThrottleBytesPerSecond, readThrottleBytesPerSecond);
        } else {
            // don't create a GlobalTrafficShapingHandler if throttling was not enabled and is still not enabled
            if (readThrottleBytesPerSecond > 0 || writeThrottleBytesPerSecond > 0) {
                globalTrafficShapingHandler = createGlobalTrafficShapingHandler(transportProtocol, readThrottleBytesPerSecond, writeThrottleBytesPerSecond);
            }
        }
    }

    public long getReadThrottle() {
        return globalTrafficShapingHandler.getReadLimit();
    }

    public long getWriteThrottle() {
        return globalTrafficShapingHandler.getWriteLimit();
    }

    public int getMaxInitialLineLength() {
        return maxInitialLineLength;
    }

    public int getMaxHeaderSize() {
        return maxHeaderSize;
    }

    public int getMaxChunkSize() {
        return maxChunkSize;
    }

    public boolean isAllowRequestsToOriginServer() {
        return allowRequestsToOriginServer;
    }

    @Override
    public HttpProxyServerBootstrap clone() {
        return new DefaultHttpProxyServerBootstrap(serverGroup,
                transportProtocol,
                new InetSocketAddress(requestedAddress.getAddress(),
                        requestedAddress.getPort() == 0 ? 0 : requestedAddress.getPort() + 1),
                sslEngineSource,
                authenticateSslClients,
                proxyAuthenticator,
                chainProxyManager,
                mitmManager,
                filtersSource,
                transparent,
                idleConnectionTimeout,
                activityTrackers,
                connectTimeout,
                serverResolver,
                globalTrafficShapingHandler != null ? globalTrafficShapingHandler.getReadLimit() : 0,
                globalTrafficShapingHandler != null ? globalTrafficShapingHandler.getWriteLimit() : 0,
                localAddress,
                proxyAlias,
                maxInitialLineLength,
                maxHeaderSize,
                maxChunkSize,
                allowRequestsToOriginServer);
    }

    @Override
    public void stop() {
        doStop(true);
    }

    @Override
    public void abort() {
        doStop(false);
    }

    /**
     * Performs cleanup necessary to stop the server. Closes all channels opened by the server and unregisters this
     * server from the server group.
     *
     * @param graceful when true, waits for requests to terminate before stopping the server
     */
    protected void doStop(boolean graceful) {
        // only stop the server if it hasn't already been stopped
        if (stopped.compareAndSet(false, true)) {
            if (graceful) {
                LOG.info("Shutting down proxy server gracefully");
            } else {
                LOG.info("Shutting down proxy server immediately (non-graceful)");
            }

            closeAllChannels(graceful);

            serverGroup.unregisterProxyServer(this, graceful);

            // remove the shutdown hook that was added when the proxy was started, since it has now been stopped
            try {
                Runtime.getRuntime().removeShutdownHook(jvmShutdownHook);
            } catch (IllegalStateException e) {
                // ignore -- IllegalStateException means the VM is already shutting down
            }

            LOG.info("Done shutting down proxy server");
        }
    }

    /**
     * Register a new {@link Channel} with this server, for later closing.
     *
     * @param channel
     */
    protected void registerChannel(Channel channel) {
        allChannels.add(channel);
    }

    /**
     * Closes all channels opened by this proxy server.
     *
     * @param graceful when false, attempts to shutdown all channels immediately and ignores any channel-closing exceptions
     */
    protected void closeAllChannels(boolean graceful) {
        LOG.info("Closing all channels " + (graceful ? "(graceful)" : "(non-graceful)"));

        ChannelGroupFuture future = allChannels.close();

        // if this is a graceful shutdown, log any channel closing failures. if this isn't a graceful shutdown, ignore them.
        if (graceful) {
            try {
                future.await(10, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();

                LOG.warn("Interrupted while waiting for channels to shut down gracefully.");
            }

            if (!future.isSuccess()) {
                for (ChannelFuture cf : future) {
                    if (!cf.isSuccess()) {
                        LOG.info("Unable to close channel.  Cause of failure for {} is {}", cf.channel(), cf.cause());
                    }
                }
            }
        }
    }

    private HttpProxyServer start() {
        if (!serverGroup.isStopped()) {
            LOG.info("Starting proxy at address: " + this.requestedAddress);

            serverGroup.registerProxyServer(this);

            doStart();
        } else {
            throw new IllegalStateException("Attempted to start proxy, but proxy's server group is already stopped");
        }

        return this;
    }

    private void doStart() {
        ServerBootstrap serverBootstrap = new ServerBootstrap().group(
                serverGroup.getClientToProxyAcceptorPoolForTransport(transportProtocol),
                serverGroup.getClientToProxyWorkerPoolForTransport(transportProtocol));

        ChannelInitializer<Channel> initializer = new ChannelInitializer<Channel>() {
            protected void initChannel(Channel ch) throws Exception {
                new ClientToProxyConnection(
                        DefaultHttpProxyServer.this,
                        sslEngineSource,
                        authenticateSslClients,
                        ch.pipeline(),
                        globalTrafficShapingHandler);
            }
        };
        switch (transportProtocol) {
            case TCP:
                LOG.info("Proxy listening with TCP transport");
                serverBootstrap.channelFactory(new ChannelFactory<ServerChannel>() {
                    @Override
                    public ServerChannel newChannel() {
                        return new NioServerSocketChannel();
                    }
                });
                break;
            case UDT:
                LOG.info("Proxy listening with UDT transport");
//                serverBootstrap.channelFactory(NioUdtProvider.BYTE_ACCEPTOR)
//                        .option(ChannelOption.SO_BACKLOG, 10)
//                        .option(ChannelOption.SO_REUSEADDR, true);

//                break;
                throw new UnsupportedOperationException("un support  UDT transport");
            default:
                throw new UnknownTransportProtocolException(transportProtocol);
        }
        serverBootstrap.childHandler(initializer);
        ChannelFuture future = serverBootstrap.bind(requestedAddress)
                .addListener(new ChannelFutureListener() {
                    @Override
                    public void operationComplete(ChannelFuture future)
                            throws Exception {
                        if (future.isSuccess()) {
                            registerChannel(future.channel());
                        }
                    }
                }).awaitUninterruptibly();

        Throwable cause = future.cause();
        if (cause != null) {
            throw new RuntimeException(cause);
        }

        this.boundAddress = ((InetSocketAddress) future.channel().localAddress());
        LOG.info("Proxy started at address: " + this.boundAddress);

        Runtime.getRuntime().addShutdownHook(jvmShutdownHook);
    }

    protected ChainedProxyManager getChainProxyManager() {
        return chainProxyManager;
    }

    protected MitmManager getMitmManager() {
        return mitmManager;
    }

    protected SslEngineSource getSslEngineSource() {
        return sslEngineSource;
    }

    protected ProxyAuthenticator getProxyAuthenticator() {
        return proxyAuthenticator;
    }

    public HttpFiltersSource getFiltersSource() {
        return filtersSource;
    }

    protected Collection<ActivityTracker> getActivityTrackers() {
        return activityTrackers;
    }

    public String getProxyAlias() {
        return proxyAlias;
    }


    protected EventLoopGroup getProxyToServerWorkerFor(TransportProtocol transportProtocol) {
        return serverGroup.getProxyToServerWorkerPoolForTransport(transportProtocol);
    }

    // TODO: refactor bootstrap into a separate class
    private static class DefaultHttpProxyServerBootstrap implements HttpProxyServerBootstrap {
        private String name = "LittleProxy";
        private ServerGroup serverGroup = null;
        private TransportProtocol transportProtocol = TransportProtocol.TCP;
        private InetSocketAddress requestedAddress;
        private int port = 8080;
        private boolean allowLocalOnly = true;
        private SslEngineSource sslEngineSource = null;
        private boolean authenticateSslClients = true;
        private ProxyAuthenticator proxyAuthenticator = null;
        private ChainedProxyManager chainProxyManager = null;
        private MitmManager mitmManager = null;
        private HttpFiltersSource filtersSource = new HttpFiltersSourceAdapter();
        private boolean transparent = false;
        private int idleConnectionTimeout = 70;
        private Collection<ActivityTracker> activityTrackers = new ConcurrentLinkedQueue<ActivityTracker>();
        private int connectTimeout = 40000;
        private HostResolver serverResolver = new DefaultHostResolver();
        private long readThrottleBytesPerSecond;
        private long writeThrottleBytesPerSecond;
        private InetSocketAddress localAddress;
        private String proxyAlias;
        private int clientToProxyAcceptorThreads = ServerGroup.DEFAULT_INCOMING_ACCEPTOR_THREADS;
        private int clientToProxyWorkerThreads = ServerGroup.DEFAULT_INCOMING_WORKER_THREADS;
        private int proxyToServerWorkerThreads = ServerGroup.DEFAULT_OUTGOING_WORKER_THREADS;
        private int maxInitialLineLength = MAX_INITIAL_LINE_LENGTH_DEFAULT;
        private int maxHeaderSize = MAX_HEADER_SIZE_DEFAULT;
        private int maxChunkSize = MAX_CHUNK_SIZE_DEFAULT;
        private boolean allowRequestToOriginServer = false;

        private DefaultHttpProxyServerBootstrap() {
        }

        private DefaultHttpProxyServerBootstrap(
                ServerGroup serverGroup,
                TransportProtocol transportProtocol,
                InetSocketAddress requestedAddress,
                SslEngineSource sslEngineSource,
                boolean authenticateSslClients,
                ProxyAuthenticator proxyAuthenticator,
                ChainedProxyManager chainProxyManager,
                MitmManager mitmManager,
                HttpFiltersSource filtersSource,
                boolean transparent, int idleConnectionTimeout,
                Collection<ActivityTracker> activityTrackers,
                int connectTimeout, HostResolver serverResolver,
                long readThrottleBytesPerSecond,
                long writeThrottleBytesPerSecond,
                InetSocketAddress localAddress,
                String proxyAlias,
                int maxInitialLineLength,
                int maxHeaderSize,
                int maxChunkSize,
                boolean allowRequestToOriginServer) {
            this.serverGroup = serverGroup;
            this.transportProtocol = transportProtocol;
            this.requestedAddress = requestedAddress;
            this.port = requestedAddress.getPort();
            this.sslEngineSource = sslEngineSource;
            this.authenticateSslClients = authenticateSslClients;
            this.proxyAuthenticator = proxyAuthenticator;
            this.chainProxyManager = chainProxyManager;
            this.mitmManager = mitmManager;
            this.filtersSource = filtersSource;
            this.transparent = transparent;
            this.idleConnectionTimeout = idleConnectionTimeout;
            if (activityTrackers != null) {
                this.activityTrackers.addAll(activityTrackers);
            }
            this.connectTimeout = connectTimeout;
            this.serverResolver = serverResolver;
            this.readThrottleBytesPerSecond = readThrottleBytesPerSecond;
            this.writeThrottleBytesPerSecond = writeThrottleBytesPerSecond;
            this.localAddress = localAddress;
            this.proxyAlias = proxyAlias;
            this.maxInitialLineLength = maxInitialLineLength;
            this.maxHeaderSize = maxHeaderSize;
            this.maxChunkSize = maxChunkSize;
            this.allowRequestToOriginServer = allowRequestToOriginServer;
        }

        private DefaultHttpProxyServerBootstrap(Properties props) {
            this.withUseDnsSec(ProxyUtils.extractBooleanDefaultFalse(
                    props, "dnssec"));
            this.transparent = ProxyUtils.extractBooleanDefaultFalse(
                    props, "transparent");
            this.idleConnectionTimeout = ProxyUtils.extractInt(props,
                    "idle_connection_timeout");
            this.connectTimeout = ProxyUtils.extractInt(props,
                    "connect_timeout", 0);
            this.maxInitialLineLength = ProxyUtils.extractInt(props,
                    "max_initial_line_length", MAX_INITIAL_LINE_LENGTH_DEFAULT);
            this.maxHeaderSize = ProxyUtils.extractInt(props,
                    "max_header_size", MAX_HEADER_SIZE_DEFAULT);
            this.maxChunkSize = ProxyUtils.extractInt(props,
                    "max_chunk_size", MAX_CHUNK_SIZE_DEFAULT);
        }

        @Override
        public HttpProxyServerBootstrap withName(String name) {
            this.name = name;
            return this;
        }

        @Override
        public HttpProxyServerBootstrap withTransportProtocol(
                TransportProtocol transportProtocol) {
            this.transportProtocol = transportProtocol;
            return this;
        }

        @Override
        public HttpProxyServerBootstrap withAddress(InetSocketAddress address) {
            this.requestedAddress = address;
            return this;
        }

        @Override
        public HttpProxyServerBootstrap withPort(int port) {
            this.requestedAddress = null;
            this.port = port;
            return this;
        }

        @Override
        public HttpProxyServerBootstrap withNetworkInterface(InetSocketAddress inetSocketAddress) {
            this.localAddress = inetSocketAddress;
            return this;
        }

        @Override
        public HttpProxyServerBootstrap withProxyAlias(String alias) {
            this.proxyAlias = alias;
            return this;
        }

        @Override
        public HttpProxyServerBootstrap withAllowLocalOnly(
                boolean allowLocalOnly) {
            this.allowLocalOnly = allowLocalOnly;
            return this;
        }

        @Override
        @Deprecated
        public HttpProxyServerBootstrap withListenOnAllAddresses(boolean listenOnAllAddresses) {
            LOG.warn("withListenOnAllAddresses() is deprecated and will be removed in a future release. Use withNetworkInterface().");
            return this;
        }

        @Override
        public HttpProxyServerBootstrap withSslEngineSource(
                SslEngineSource sslEngineSource) {
            this.sslEngineSource = sslEngineSource;
            if (this.mitmManager != null) {
                LOG.warn("Enabled encrypted inbound connections with man in the middle. "
                        + "These are mutually exclusive - man in the middle will be disabled.");
                this.mitmManager = null;
            }
            return this;
        }

        @Override
        public HttpProxyServerBootstrap withAuthenticateSslClients(
                boolean authenticateSslClients) {
            this.authenticateSslClients = authenticateSslClients;
            return this;
        }

        @Override
        public HttpProxyServerBootstrap withProxyAuthenticator(
                ProxyAuthenticator proxyAuthenticator) {
            this.proxyAuthenticator = proxyAuthenticator;
            return this;
        }

        @Override
        public HttpProxyServerBootstrap withChainProxyManager(
                ChainedProxyManager chainProxyManager) {
            this.chainProxyManager = chainProxyManager;
            return this;
        }

        @Override
        public HttpProxyServerBootstrap withManInTheMiddle(
                MitmManager mitmManager) {
            this.mitmManager = mitmManager;
            if (this.sslEngineSource != null) {
                LOG.warn("Enabled man in the middle with encrypted inbound connections. "
                        + "These are mutually exclusive - encrypted inbound connections will be disabled.");
                this.sslEngineSource = null;
            }
            return this;
        }

        @Override
        public HttpProxyServerBootstrap withFiltersSource(
                HttpFiltersSource filtersSource) {
            this.filtersSource = filtersSource;
            return this;
        }

        @Override
        public HttpProxyServerBootstrap withUseDnsSec(boolean useDnsSec) {
            if (useDnsSec) {
                this.serverResolver = new DnsSecServerResolver();
            } else {
                this.serverResolver = new DefaultHostResolver();
            }
            return this;
        }

        @Override
        public HttpProxyServerBootstrap withTransparent(
                boolean transparent) {
            this.transparent = transparent;
            return this;
        }

        @Override
        public HttpProxyServerBootstrap withIdleConnectionTimeout(
                int idleConnectionTimeout) {
            this.idleConnectionTimeout = idleConnectionTimeout;
            return this;
        }

        @Override
        public HttpProxyServerBootstrap withConnectTimeout(
                int connectTimeout) {
            this.connectTimeout = connectTimeout;
            return this;
        }

        @Override
        public HttpProxyServerBootstrap withServerResolver(
                HostResolver serverResolver) {
            this.serverResolver = serverResolver;
            return this;
        }

        @Override
        public HttpProxyServerBootstrap plusActivityTracker(
                ActivityTracker activityTracker) {
            activityTrackers.add(activityTracker);
            return this;
        }

        @Override
        public HttpProxyServerBootstrap withThrottling(long readThrottleBytesPerSecond, long writeThrottleBytesPerSecond) {
            this.readThrottleBytesPerSecond = readThrottleBytesPerSecond;
            this.writeThrottleBytesPerSecond = writeThrottleBytesPerSecond;
            return this;
        }

        @Override
        public HttpProxyServerBootstrap withMaxInitialLineLength(int maxInitialLineLength) {
            this.maxInitialLineLength = maxInitialLineLength;
            return this;
        }

        @Override
        public HttpProxyServerBootstrap withMaxHeaderSize(int maxHeaderSize) {
            this.maxHeaderSize = maxHeaderSize;
            return this;
        }

        @Override
        public HttpProxyServerBootstrap withMaxChunkSize(int maxChunkSize) {
            this.maxChunkSize = maxChunkSize;
            return this;
        }

        @Override
        public HttpProxyServerBootstrap withAllowRequestToOriginServer(boolean allowRequestToOriginServer) {
            this.allowRequestToOriginServer = allowRequestToOriginServer;
            return this;
        }

        @Override
        public HttpProxyServer start() {
            return build().start();
        }

        @Override
        public HttpProxyServerBootstrap withThreadPoolConfiguration(ThreadPoolConfiguration configuration) {
            this.clientToProxyAcceptorThreads = configuration.getAcceptorThreads();
            this.clientToProxyWorkerThreads = configuration.getClientToProxyWorkerThreads();
            this.proxyToServerWorkerThreads = configuration.getProxyToServerWorkerThreads();
            return this;
        }

        private DefaultHttpProxyServer build() {
            final ServerGroup serverGroup;

            if (this.serverGroup != null) {
                serverGroup = this.serverGroup;
            } else {
                serverGroup = new ServerGroup(name, clientToProxyAcceptorThreads, clientToProxyWorkerThreads, proxyToServerWorkerThreads);
            }

            return new DefaultHttpProxyServer(serverGroup,
                    transportProtocol, determineListenAddress(),
                    sslEngineSource, authenticateSslClients,
                    proxyAuthenticator, chainProxyManager, mitmManager,
                    filtersSource, transparent,
                    idleConnectionTimeout, activityTrackers, connectTimeout,
                    serverResolver, readThrottleBytesPerSecond, writeThrottleBytesPerSecond,
                    localAddress, proxyAlias, maxInitialLineLength, maxHeaderSize, maxChunkSize,
                    allowRequestToOriginServer);
        }

        private InetSocketAddress determineListenAddress() {
            if (requestedAddress != null) {
                return requestedAddress;
            } else {
                // Binding only to localhost can significantly improve the
                // security of the proxy.
                if (allowLocalOnly) {
                    return new InetSocketAddress("127.0.0.1", port);
                } else {
                    return new InetSocketAddress(port);
                }
            }
        }
    }
}
