javaapache-httpclient-5.x

Configure custom socket factories for both TLS and non-TLS with apache HttpClient 5.4.2


Looking for the possibility to configure the custom socket factories to set traffic class or type-of-service octet in the IP header for packets sent from this Socket.

The below setup worked so far with below apache HttpClient versions

org.apache.httpcomponents.client5 5.3.1

org.apache.httpcomponents.core5 5.2.4

DSCP marking for HTTP

import java.io.IOException;
import java.net.Proxy;
import java.net.Socket;
import java.net.SocketException;

import org.apache.hc.client5.http.socket.PlainConnectionSocketFactory;
import org.apache.hc.core5.http.protocol.HttpContext;


class CustomDscpPlainConnectionSocketFactory extends PlainConnectionSocketFactory {

    private int dscpValue;

    CustomDscpPlainConnectionSocketFactory(int dscpValue) {
        this.dscpValue = dscpValue;
    }

    @Override
    public Socket createSocket(final HttpContext context) throws IOException {
        Socket socket = super.createSocket(context);
        socket.setTrafficClass(dscpValue << 2);
        return socket;
    }

    @Override
    public Socket createSocket(final Proxy proxy, final HttpContext context) throws IOException {
        Socket socket = super.createSocket(proxy, context);
        socket.setTrafficClass(dscpValue << 2);
        return socket;
    }
}

DSCP marking for HTTPS

import java.io.IOException;
import java.net.Proxy;
import java.net.Socket;
import java.net.SocketException;

import javax.net.ssl.SSLContext;

import org.apache.hc.client5.http.ssl.HttpsSupport;
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
import org.apache.hc.core5.http.protocol.HttpContext;

class CustomDscpSSLConnectionSocketFactory extends SSLConnectionSocketFactory {

    private int dscpValue;

    public CustomDscpSSLConnectionSocketFactory(final int dscpValue,
            final SSLContext sslContext,
            final String[] supportedProtocols) {
        super(sslContext.getSocketFactory(), supportedProtocols,
                getCipherSuites().toArray(String[]::new),
                HttpsSupport.getDefaultHostnameVerifier());
        this.dscpValue = dscpValue;
    }

    public CustomDscpSSLConnectionSocketFactory(SSLContext sslContext) {
        super(sslContext);
    }

    @Override
    public Socket createSocket(HttpContext context) throws IOException {
        Socket socket = super.createSocket(context);
        socket.setTrafficClass(dscpValue << 2);
        return socket;
    }

    @Override
    public Socket createSocket(final Proxy proxy, final HttpContext context) throws IOException {
        Socket socket = super.createSocket(proxy, context);
        socket.setTrafficClass(dscpValue << 2);
        return socket;
    }
}

HttpClient

import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.client5.http.socket.ConnectionSocketFactory;
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;

var plainSocketFactory = new CustomDscpPlainConnectionSocketFactory(20);
var sslConnSocketFactory =  new CustomDscpSSLConnectionSocketFactory(20, sslContext, getProtocols())
PoolingHttpClientConnectionManager poolingmgr = new PoolingHttpClientConnectionManager(
                RegistryBuilder.<ConnectionSocketFactory>create()
                        .register(URIScheme.HTTP.id, plainSocketFactory)
                        .register(URIScheme.HTTPS.id, sslConnSocketFactory)
                        .build());
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create()
                .setRedirectStrategy(DefaultRedirectStrategy.INSTANCE)
                .setConnectionManager(poolingmgr);
CloseableHttpClient httpClient = httpClientBuilder.build();

Would like to achieve the similar setup with latest apache HTTP client update versions where PlainConnectionSocketFactory and SSLConnectionSocketFactory are deprecated.

org.apache.httpcomponents.client5 5.4.2

org.apache.httpcomponents.core5 5.3.3

DSCP marking for HTTPS which works

import java.net.Socket;
import java.net.SocketException;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;

import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
import org.apache.hc.client5.http.ssl.HttpsSupport;
import org.apache.hc.core5.reactor.ssl.SSLBufferMode;


    public class CustomClientTlsStrategy extends DefaultClientTlsStrategy {
    
        private int dscpValue;
    
        public CustomClientTlsStrategy(final int dscpValue,
                final SSLContext sslContext,
                final String[] supportedProtocols) {
            super(sslContext, supportedProtocols,
                   getCipherSuites().toArray(String[]::new),
                    SSLBufferMode.STATIC, HttpsSupport.getDefaultHostnameVerifier());
            this.dscpValue = dscpValue;
        }
    
        protected void initializeSocket(final SSLSocket socket) {
          socket.setTrafficClass(dscpValue << 2);
        }
    }

DSCP marking for HTTP doesn't work and may be the below setup is not valid for HTTP.

import java.io.IOException;
import java.net.Socket;
import java.net.SocketException;

import org.apache.hc.client5.http.impl.io.ManagedHttpClientConnectionFactory;
import org.apache.hc.client5.http.io.ManagedHttpClientConnection;

    public class CustomManagedHttpClientConnectionFactory extends ManagedHttpClientConnectionFactory {
    
        private int dscpValue;
        public CustomManagedHttpClientConnectionFactory(int dscpValue) {
            super();
            this.dscpValue = dscpValue;
        }
        @Override
        public ManagedHttpClientConnection createConnection(final Socket socket) throws IOException {
            ManagedHttpClientConnection connection = super.createConnection(socket);
            if (connection.getSocket() != null ) { // Socket here is always null and thus DSCP is never marked.
                connection.getSocket().setTrafficClass(dscpValue << 2);
            }
            return connection;
        }
    }

HttpClient

CustomManagedHttpClientConnectionFactory customManagedHttpClientConnectionFactory = new CustomManagedHttpClientConnectionFactory(20));
var poolingmgrBuilder = PoolingHttpClientConnectionManagerBuilder.create();
poolingmgrBuilder.setTlsSocketStrategy(buildCustomTlsStrategy());
poolingmgrBuilder.setConnectionFactory(customManagedHttpClientConnectionFactory);

var poolingmgr = poolingmgrBuilder.build();

HttpClientBuilder httpClientBuilder = HttpClientBuilder.create()
                .setRedirectStrategy(DefaultRedirectStrategy.INSTANCE)
                .setConnectionManager(poolingmgr);
CloseableHttpClient httpClient = httpClientBuilder.build();

Solution

  • One needs a custom HttpClientConnectionOperator to accomplish that. In your case you many get away with a single extra lambda that creates a detached Socket.

    PoolingHttpClientConnectionManagerBuilder connectionManagerBuilder = new PoolingHttpClientConnectionManagerBuilder() {
    
        @Override
        protected HttpClientConnectionOperator createConnectionOperator(SchemePortResolver schemePortResolver, DnsResolver dnsResolver, TlsSocketStrategy tlsSocketStrategy) {
            return new DefaultHttpClientConnectionOperator(
                    proxy -> {
                        Socket socket = proxy != null ? new Socket(proxy) : new Socket();
                        // Apply custom socket config here
                        return socket;
                    },
                    schemePortResolver,
                    dnsResolver,
                    RegistryBuilder.<TlsSocketStrategy>create()
                            .register(URIScheme.HTTPS.id, tlsSocketStrategy)
                            .build());
        }
    };
    PoolingHttpClientConnectionManager connectionManager = connectionManagerBuilder.build();