NettyConnectorConfiguration.java

/*
 * Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the
 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
 * version 2 with the GNU Classpath Exception, which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 */

package org.glassfish.jersey.netty.connector;

import io.netty.handler.codec.http.DefaultHttpHeaders;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.proxy.HttpProxyHandler;
import io.netty.handler.proxy.ProxyHandler;
import org.glassfish.jersey.client.ClientProperties;
import org.glassfish.jersey.client.ClientRequest;
import org.glassfish.jersey.client.innate.ClientProxy;
import org.glassfish.jersey.internal.util.collection.Ref;
import org.glassfish.jersey.client.innate.ConnectorConfiguration;

import javax.ws.rs.ProcessingException;
import javax.ws.rs.client.Client;
import java.net.InetSocketAddress;
import java.net.URI;
import java.util.Map;

class NettyConnectorConfiguration<N extends NettyConnectorConfiguration<N>> extends ConnectorConfiguration<N> {

    /* package */ final NullableRef<NettyConnectionController> connectionController = NullableRef.empty();
    /* package */ final NullableRef<Boolean> enableHostnameVerification = NullableRef.empty();
    /* package */ final Ref<Integer> expect100ContTimeout = NullableRef.empty();
    /* package */ final NullableRef<Boolean> filterHeadersForProxy = NullableRef.empty();
    /* package */ final NullableRef<Integer> firstHttpHeaderLineLength = NullableRef.empty();
    /* package */ final Ref<Boolean> loggingEnabled = NullableRef.empty();
    /* package */ final NullableRef<Integer> maxChunkSize = NullableRef.empty();
    /* package */ final NullableRef<Integer> maxHeaderSize = NullableRef.empty();
    // either from Jersey config, or default
    /* package */ final Ref<Integer> maxPoolSizeTotal = NullableRef.empty();
    // either from Jersey config, or default
    /* package */ final Ref<Integer> maxPoolIdle = NullableRef.empty();
    // either from system property, or from Jersey config, or default
    /* package */ final Ref<Integer> maxPoolSize = NullableRef.empty();
    /* package */ final Ref<Integer> maxRedirects = NullableRef.empty();
    /* package */ final NullableRef<Boolean> preserveMethodOnRedirect = NullableRef.empty();
    /* package */ final NullableRef<NettyHttpRedirectController> redirectController = NullableRef.empty();

    // If HTTP keepalive is enabled the value of "http.maxConnections" determines the maximum number
    // of idle connections that will be simultaneously kept alive, per destination.
    private static final String HTTP_KEEPALIVE_STRING = System.getProperty("http.keepAlive");
    // http.keepalive (default: true)
    private static final Boolean HTTP_KEEPALIVE =
            HTTP_KEEPALIVE_STRING == null ? Boolean.TRUE : Boolean.parseBoolean(HTTP_KEEPALIVE_STRING);

    // http.maxConnections (default: 5)
    private static final int DEFAULT_MAX_POOL_SIZE = 5;
    private static final int MAX_POOL_SIZE = Integer.getInteger("http.maxConnections", DEFAULT_MAX_POOL_SIZE);
    private static final int DEFAULT_MAX_POOL_IDLE = 60; // seconds
    private static final int DEFAULT_MAX_POOL_SIZE_TOTAL = 60; // connections

    private static final int DEFAULT_MAX_REDIRECTS = 5;

    /**
     * Set the connection pooling controller for the Netty Connector.
     *
     * @param controller the connection pooling controller.
     * @return updated configuration.
     */
    public N connectionController(NettyConnectionController controller) {
        connectionController.set(controller);
        return self();
    }

    /**
     * This setting determines waiting time in milliseconds for 100-Continue response when 100-Continue is sent by the client.
     * The property {@link NettyClientProperties#EXPECT_100_CONTINUE_TIMEOUT} has precedence over this setting.
     *
     * @param millis the timeout for 100-Continue response.
     * @return updated configuration.
     */
    public N expect100ContinueTimeout(int millis) {
        expect100ContTimeout.set(millis);
        return self();
    }

    /**
     * Enable or disable the endpoint identification algorithm to HTTPS. The property
     * {@link NettyClientProperties#ENABLE_SSL_HOSTNAME_VERIFICATION} takes precedence over this setting.
     *
     * @param enable enable or disable the hostname verification.
     * @return updated configuration.
     */
    public N enableSslHostnameVerification(boolean enable) {
        enableHostnameVerification.set(enable);
        return self();
    }

    /**
     * Enable or disable the Netty logging by {@code LoggingHandler(Level.DEBUG)}. Disabled by default.
     * The property {@link NettyClientProperties#LOGGING_ENABLED} takes precedence over this setting.
     *
     * @param enable to enable or disable.
     * @return updated configuration.
     */
    public N enableLoggingHandler(boolean enable) {
        loggingEnabled.set(enable);
        return self();
    }

    /**
     * Filter the HTTP headers for requests (CONNECT) towards the proxy except for PROXY-prefixed
     * and HOST headers when {@code true}.
     * The property {@link NettyClientProperties#FILTER_HEADERS_FOR_PROXY} has precedence over this setting.
     *
     * @param filter to filter or not. The default is {@code true}.
     * @return updated configuration.
     */
    public N filterHeadersForProxy(boolean filter) {
        filterHeadersForProxy.set(filter);
        return self();
    }

    /**
     * This property determines the number of seconds the idle connections are kept in the pool before pruned.
     * The default is 60. Specify 0 to disable. The property {@link NettyClientProperties#IDLE_CONNECTION_PRUNE_TIMEOUT}
     * has precedence over this setting.
     *
     * @param seconds the timeout in seconds.
     * @return updated configuration.
     */
    public N idleConnectionPruneTimeout(int seconds) {
        maxPoolIdle.set(seconds);
        return self();
    }

    /**
     * Set the maximum length of the first line of the HTTP header.
     * The property {@link NettyClientProperties#MAX_INITIAL_LINE_LENGTH} has precedence over this setting.
     *
     * @param length the length of the first line of the HTTP header.
     * @return updated configuration.
     */
    public N initialHttpHeaderLineLength(int length) {
        firstHttpHeaderLineLength.set(length);
        return self();
    }

    /**
     * Set the maximum chunk size for the Netty connector. The property {@link NettyClientProperties#MAX_CHUNK_SIZE}
     * has precedence over this setting.
     *
     * @param size the new size of chunks.
     * @return updated configuration.
     */
    public N maxChunkSize(int size) {
        maxChunkSize.set(size);
        return self();
    }

    /**
     * This setting determines the maximum number of idle connections that will be simultaneously kept alive, per destination.
     * The default is 5. The property {@link NettyClientProperties#MAX_CONNECTIONS} takes precedence over this setting.
     *
     * @param maxCount maximum number of idle connections per destination.
     * @return updated configuration.
     */
    public N maxConnectionsPerDestination(int maxCount) {
        maxPoolSize.set(maxCount);
        return self();
    }

    /**
     * Set the maximum header size in bytes for the HTTP headers processed by Netty.
     * The property {@link NettyClientProperties#MAX_HEADER_SIZE} has precedence over this setting.
     *
     * @param size the new maximum header size.
     * @return updated configuration.
     */
    public N maxHeaderSize(int size) {
        maxHeaderSize.set(size);
        return self();
    }

    /**
     * Set the maximum number of redirects to prevent infinite redirect loop. The default is 5.
     * The property {@link NettyClientProperties#MAX_REDIRECTS} has precedence over this setting.
     *
     * @param max the maximum number of redirects.
     * @return updated configuration.
     */
    public N maxRedirects(int max) {
        maxRedirects.set(max);
        return self();
    }

    /**
     * Set the maximum number of idle connections that will be simultaneously kept alive. The property
     * {@link NettyClientProperties#MAX_CONNECTIONS_TOTAL} has precedence over this setting.
     *
     * @param max the maximum number of idle connections.
     * @return updated configuration.
     */
    public N maxTotalConnections(int max) {
        maxPoolSizeTotal.set(max);
        return self();
    }

    /**
     * Set the preservation of methods during HTTP redirect.
     * By default, the HTTP POST request are not transformed into HTTP GET for status 301 & 302.
     * The property {@link NettyClientProperties#PRESERVE_METHOD_ON_REDIRECT} has precedence over this setting.
     *
     * @param preserve to preserve or not to preserve.
     * @return updated configuration.
     */
    public N preserveMethodOnRedirect(boolean preserve) {
        preserveMethodOnRedirect.set(preserve);
        return self();
    }

    /**
     * Set the Netty Connector HTTP redirect controller.
     * The property {@link NettyClientProperties#HTTP_REDIRECT_CONTROLLER} has precedence over this setting.
     *
     * @param controller the HTTP redirect controller.
     * @return updated configuration.
     */
    public N redirectController(NettyHttpRedirectController controller) {
        redirectController.set(controller);
        return self();
    }

    @SuppressWarnings("unchecked")
    protected N self() {
        return (N) this;
    }

    abstract static class ReadWrite<N extends ReadWrite<N>>
            extends NettyConnectorConfiguration<N>
            implements ConnectorConfiguration.Read<N> {

        @Override
        public <X extends ConnectorConfiguration<?>> void setNonEmpty(X otherCC) {
            NettyConnectorConfiguration<?> other = (NettyConnectorConfiguration<?>) otherCC;
            ConnectorConfiguration.Read.super.setNonEmpty(other);
            this.connectionController.setNonEmpty(other.connectionController);
            this.redirectController.setNonEmpty(other.redirectController);
            this.connectionController.setNonEmpty(other.connectionController);
            this.enableHostnameVerification.setNonEmpty(other.enableHostnameVerification);
            ((NullableRef<Integer>) this.expect100ContTimeout).setNonEmpty((NullableRef<Integer>) other.expect100ContTimeout);
            this.filterHeadersForProxy.setNonEmpty(other.filterHeadersForProxy);
            this.firstHttpHeaderLineLength.setNonEmpty(other.firstHttpHeaderLineLength);
            ((NullableRef<Boolean>) this.loggingEnabled).setNonEmpty((NullableRef<Boolean>) other.loggingEnabled);
            this.maxChunkSize.setNonEmpty(other.maxChunkSize);
            this.maxHeaderSize.setNonEmpty(other.maxHeaderSize);
            ((NullableRef<Integer>) this.maxPoolIdle).setNonEmpty((NullableRef<Integer>) other.maxPoolIdle);
            ((NullableRef<Integer>) this.maxPoolSize).setNonEmpty((NullableRef<Integer>) other.maxPoolSize);
            ((NullableRef<Integer>) this.maxPoolSizeTotal).setNonEmpty((NullableRef<Integer>) other.maxPoolSizeTotal);
            ((NullableRef<Integer>) this.maxRedirects).setNonEmpty((NullableRef<Integer>) other.maxRedirects);
            this.preserveMethodOnRedirect.setNonEmpty(other.preserveMethodOnRedirect);
            this.redirectController.setNonEmpty(other.redirectController);
        }

        @Override
        public N init() {
            Read.super.init();
            enableSslHostnameVerification(Boolean.TRUE);
            expect100ContinueTimeout(NettyClientProperties.DEFAULT_EXPECT_100_CONTINUE_TIMEOUT_VALUE);
            filterHeadersForProxy(Boolean.TRUE);
            initialHttpHeaderLineLength(NettyClientProperties.DEFAULT_INITIAL_LINE_LENGTH);
            enableLoggingHandler(Boolean.FALSE);
            maxChunkSize(NettyClientProperties.DEFAULT_CHUNK_SIZE);
            maxHeaderSize(NettyClientProperties.DEFAULT_HEADER_SIZE);
            // either from Jersey config, or default
            maxTotalConnections(DEFAULT_MAX_POOL_SIZE_TOTAL);
            // either from Jersey config, or default
            maxPoolIdle.set(DEFAULT_MAX_POOL_IDLE);
            // either from system property, or from Jersey config, or default
            maxPoolSize.set(HTTP_KEEPALIVE ? MAX_POOL_SIZE : DEFAULT_MAX_POOL_SIZE);
            maxRedirects(DEFAULT_MAX_REDIRECTS);
            preserveMethodOnRedirect(Boolean.TRUE);
            return self();
        }

        /**
         * Get the preset {@link NettyConnectionController} or create an instance of the default one, if not preset.
         * @return the {@link NettyConnectionController} instance.
         */
        /* package */ NettyConnectionController connectionController() {
            return connectionController.isPresent() ? connectionController.get() : new NettyConnectionController();
        }

        /**
         * Update {@link #expect100ContinueTimeout(int) expect 100-Continue timeout} based on current http request properties.
         *
         * @param clientRequest the current http client request.
         * @return updated configuration.
         */
        /* package */ N expect100ContinueTimeout(ClientRequest clientRequest) {
            expect100ContTimeout.set(
                    clientRequest.resolveProperty(
                            prefixed(NettyClientProperties.EXPECT_100_CONTINUE_TIMEOUT), expect100ContTimeout.get()));
            return this.self();
        }

        /**
         * Return value of {@link #enableSslHostnameVerification(boolean)} setting either from the configuration of from the
         * HTTP client request properties. The default is {@code true}.
         *
         * @param properties the HTTP client request properties.
         * @return the value of SSL hostname verification setting.
         */
        /* package */ boolean isSslHostnameVerificationEnabled(Map<String, Object> properties) {
            return ClientProperties.getValue(properties,
                    prefixed(NettyClientProperties.ENABLE_SSL_HOSTNAME_VERIFICATION),
                    enableHostnameVerification.get());
        }

        /**
         * Update {@link #maxRedirects(int)} value from the HTTP Client request.
         * @param request the HTTP Client request.
         * @return maximum redirects value.
         */
        /* package */ int maxRedirects(ClientRequest request) {
            maxRedirects.set(
                    request.resolveProperty(prefixed(NettyClientProperties.MAX_REDIRECTS), maxRedirects.get()));
            return maxRedirects.get();
        }

        /**
         * Update the {@link #preserveMethodOnRedirect(boolean) preservation} of HTTP method during HTTP redirect
         * by HTTP client request properties.
         *
         * @param request HTTP client request.
         * @return the value of preservation.
         */
        /* package */ boolean preserveMethodOnRedirect(ClientRequest request) {
            preserveMethodOnRedirect.set(
                    request.resolveProperty(
                            prefixed(NettyClientProperties.PRESERVE_METHOD_ON_REDIRECT), preserveMethodOnRedirect.get()));
            return preserveMethodOnRedirect.get();
        }

        /**
         * Get the instance of preset {@link NettyHttpRedirectController} either from configuration,
         * or from the HTTP client request, or the default if non set.
         * @param request the HTTP client request.
         * @return an instance of {@link NettyHttpRedirectController}.
         */
        /* package */ NettyHttpRedirectController redirectController(ClientRequest request) {
            NettyHttpRedirectController customRedirectController =
                    request.resolveProperty(
                            prefixed(NettyClientProperties.HTTP_REDIRECT_CONTROLLER), NettyHttpRedirectController.class);
            if (customRedirectController == null) {
                customRedirectController = redirectController.get();
            }
            if (customRedirectController == null) {
                customRedirectController = new NettyHttpRedirectController();
            }

            return customRedirectController;
        }

        /**
         * <p>
         *  Return a new instance of configuration updated by the merged settings from this and client properties.
         *  Only properties unresolved during the request are updated.
         * </p><p>
         *  {@code This} is meant to be settings from the connector.
         *  The priorities should go DEFAULTS -> CONNECTOR -> CLIENT -> REQUEST.
         * </p>
         *
         * @param client the REST client.
         * @return a new instance of configuration.
         */
        /* package */ N fromClient(Client client) {
            final Map<String, Object> properties = client.getConfiguration().getProperties();
            final N clientConfiguration = copyFromClient(client.getConfiguration());

            final Object threadPoolSize = properties.get(clientConfiguration.prefixed(ClientProperties.ASYNC_THREADPOOL_SIZE));
            if (threadPoolSize instanceof Integer && (Integer) threadPoolSize > 0) {
                clientConfiguration.asyncThreadPoolSize((Integer) threadPoolSize);
            }

            final Object maxPoolSizeTotalProperty = properties.get(
                    clientConfiguration.prefixed(NettyClientProperties.MAX_CONNECTIONS_TOTAL));
            final Object maxPoolIdleProperty = properties.get(
                    clientConfiguration.prefixed(NettyClientProperties.IDLE_CONNECTION_PRUNE_TIMEOUT));
            final Object maxPoolSizeProperty = properties.get(
                    clientConfiguration.prefixed(NettyClientProperties.MAX_CONNECTIONS));

            if (maxPoolSizeTotalProperty != null) {
                clientConfiguration.maxPoolSizeTotal.set((Integer) maxPoolSizeTotalProperty);
            }

            if (maxPoolIdleProperty != null) {
                clientConfiguration.maxPoolIdle.set((Integer) maxPoolIdleProperty);
            }

            if (maxPoolSizeProperty != null) {
                clientConfiguration.maxPoolSize.set((Integer) maxPoolSizeProperty);
            }

            if (clientConfiguration.maxPoolSizeTotal.get() < 0) {
                throw new ProcessingException(LocalizationMessages.WRONG_MAX_POOL_TOTAL(maxPoolSizeTotal.get()));
            }

            if (clientConfiguration.maxPoolSize.get() < 0) {
                throw new ProcessingException(LocalizationMessages.WRONG_MAX_POOL_SIZE(maxPoolSize.get()));
            }

            final Object logging = properties.get(clientConfiguration.prefixed(NettyClientProperties.LOGGING_ENABLED));
            if (logging instanceof Boolean) {
                clientConfiguration.loggingEnabled.set((Boolean) logging);
            } else if (logging instanceof String) {
                clientConfiguration.loggingEnabled.set(Boolean.valueOf((String) logging));
            }

            return clientConfiguration;
        }

        /**
         * <p>
         *  Return a new instance of configuration updated by the merged settings from this and HTTP client request properties.
         *  Only properties unresolved during the request are updated.
         * </p><p>
         *  {@code This} is meant to be settings from the connector.
         *  The priorities should go DEFAULTS -> CONNECTOR -> CLIENT -> REQUEST.
         * </p>

         * @param request the HTTP client request.
         * @return a new instance of configuration.
         */
        /* package */ N fromRequest(ClientRequest request) {
            final N requestConfiguration = copyFromRequest(request);

            final Boolean logging = request.resolveProperty(prefixed(NettyClientProperties.LOGGING_ENABLED), Boolean.class);
            if (logging != null) {
                requestConfiguration.loggingEnabled.set(logging);
            }

            return requestConfiguration;
        }


        /**
         * Create an instance of {@link HttpClientCodec} based on preset settings {@link #initialHttpHeaderLineLength(int)},
         * {@link #maxHeaderSize} and {@link #maxChunkSize}. The settings can be preset in the configuration or
         * on the HTTP client request.
         *
         * @param properties The HTTP client request properties.
         * @return the {@link HttpClientCodec} instance.
         */
        /* package */ HttpClientCodec createHttpClientCodec(Map<String, Object> properties) {
            firstHttpHeaderLineLength.set(ClientProperties.getValue(properties,
                    prefixed(NettyClientProperties.MAX_INITIAL_LINE_LENGTH), firstHttpHeaderLineLength.get()));
            maxHeaderSize.set(
                    ClientProperties.getValue(properties, prefixed(NettyClientProperties.MAX_HEADER_SIZE), maxHeaderSize.get()));
            maxChunkSize.set(
                    ClientProperties.getValue(properties, prefixed(NettyClientProperties.MAX_CHUNK_SIZE), maxChunkSize.get()));

            return new HttpClientCodec(firstHttpHeaderLineLength.get(), maxHeaderSize.get(), maxChunkSize.get());
        }

        /**
         * Create an instance of {@link ProxyHandler} based on HTTP request URI's port and address,
         * the preset proxy {@link #proxyUri(URI) uri}, {@link #proxyUserName(String) username},
         * and {@link #proxyPassword(String) password}.
         *
         * Can filter headers {@link #filterHeadersForProxy(boolean)}.
         *
         * @param clientProxy the Jersey {@link ClientProxy} instance.
         * @param jerseyRequest the HTTP client request containing HTTP headers to be filtered.
         * @return a Netty {@link ProxyHandler} instance.
         */
        /* package */ ProxyHandler createProxyHandler(ClientProxy clientProxy, ClientRequest jerseyRequest) {
            final URI u = clientProxy.uri();
            InetSocketAddress proxyAddr = new InetSocketAddress(u.getHost(), u.getPort() == -1 ? 8080 : u.getPort());

            filterHeadersForProxy.set(jerseyRequest
                    .resolveProperty(prefixed(NettyClientProperties.FILTER_HEADERS_FOR_PROXY), filterHeadersForProxy.get()));
            HttpHeaders httpHeaders = NettyConnector.setHeaders(
                    jerseyRequest, new DefaultHttpHeaders(), Boolean.TRUE.equals(filterHeadersForProxy.get()));

            ProxyHandler proxy = clientProxy.userName() == null ? new HttpProxyHandler(proxyAddr, httpHeaders)
                    : new HttpProxyHandler(proxyAddr, clientProxy.userName(), clientProxy.password(), httpHeaders);
            if (connectTimeout.get() > 0) {
                proxy.setConnectTimeoutMillis(connectTimeout.get());
            }

            return proxy;
        }
    }

}