[ 
https://issues.apache.org/jira/browse/HTTPCLIENT-1977?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=16812315#comment-16812315
 ] 

Oleg Kalnichevski commented on HTTPCLIENT-1977:
-----------------------------------------------

bq. Having said that, it was a bit of unexpected behaviour from the HttpClient 
because we are using the same client certificate on the second connection and 
this certificate is known before the request is even sent so one would expect 
this will be matched.

But how is connection manager supposed to know that? It maintains a pool of 
connections that can be potentially leased by multiple worker threads 
representing different users with a different security context. Message 
properties alone are not enough to determine the user identity the request 
message is being executed with. Unless the caller passes the execution context 
with a user token in it HttpClient has to assume the user identity is unknown. 

Oleg

> HttpClient does not use Keep-Alive if client SSL auth is used
> -------------------------------------------------------------
>
>                 Key: HTTPCLIENT-1977
>                 URL: https://issues.apache.org/jira/browse/HTTPCLIENT-1977
>             Project: HttpComponents HttpClient
>          Issue Type: Bug
>            Reporter: Dmitry Andrianov
>            Priority: Major
>
> When client is configured with a certificate for SSL auth, the connection 
> pool stops working properly so each request uses a new connection.
> This happens because   when {{AbstractConnPool.getPoolEntryBlocking()}} 
> method is invoked, the state is null so this code
> {code:java}
> entry = pool.getFree(state);
> if (entry == null) {
>     break;
> }{code}
> gets null back. The {{getFree}} method only returns connection with the same 
> state. So when new connection is opened, it kinda gets null state too but 
> later after the request, {{MainClientExec}} does this:
> {code:java}
> if (userToken != null) {
>     connHolder.setState(userToken);
> }{code}
> and this eventually sets state on the pool entry to the subject of client 
> certificate.
> When another request is made - the story repeats, since 
> {{AbstractConnPool.getPoolEntryBlocking()}} uses null state, it cannot find 
> matching connection in the pool (because the only one connection there HAS 
> state now) and a new connection is created. In the end pooled HTTPS 
> connections are never reused.
>  
> This is the code to reproduce:
> {code:java}
> import com.alertme.zoo.hubregistration.util.PemUtils;
> import org.apache.http.client.config.RequestConfig;
> import org.apache.http.client.methods.CloseableHttpResponse;
> import org.apache.http.client.methods.HttpGet;
> import org.apache.http.config.Registry;
> import org.apache.http.config.RegistryBuilder;
> import org.apache.http.conn.socket.ConnectionSocketFactory;
> import org.apache.http.conn.ssl.NoopHostnameVerifier;
> import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
> import org.apache.http.impl.client.CloseableHttpClient;
> import org.apache.http.impl.client.HttpClients;
> import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
> import org.apache.http.ssl.SSLContextBuilder;
> import org.apache.http.ssl.SSLContexts;
> import org.apache.http.util.EntityUtils;
> import java.security.KeyStore;
> import java.security.PrivateKey;
> import java.security.cert.X509Certificate;
> public class Test {
>     // openssl req -newkey rsa:2048 -nodes -keyout test-client.key -x509 
> -days 3650 -out test-client.crt -subj '/CN=test-client/'
>     private final static String KEY_PEM = "" +
>             "-----BEGIN PRIVATE KEY-----\n" +
>             
> "MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQC+Kzo5Ehd1vrbe\n" +
>             
> "Za+rNIqnIzWAzKr/iWFySQnGQu5SpK1nsZ5JhBK83J4RQxRS6mULrifJgG/AqMlA\n" +
>             
> "0NmCDw0C6p/uQF+VKycntULjnuvwjU04lkTmrWk3twHnvDkd+j2SH+jfzjZePv+l\n" +
>             
> "XCCQLQS5xiMreGjMx4EILUsB00EQoUFbdGg1NgTt7g8L/WuEDC9YybC6FJHOBYP/\n" +
>             
> "02wmW4REYyMuVY3SfZAsOk5YRbEf25c7IAA3+tbaXz9SuRgUK3SKI0ReGBtLyvSj\n" +
>             
> "gyoyOTwT89QOdEsL6KM9TZhjJGpmeXGMHBNP7uEXuXgfB6wKay1ejD6hr+lcgnIC\n" +
>             
> "QqHthASrAgMBAAECggEBAJw6Rwq7oipJE1KBl1+/Omk0s6+sdI6Z/kQ1XKJUOhYK\n" +
>             
> "06pscO1UY1BkrjbgNMIpbfm6iVUw/p34C94DtazzUG0k81535A5X9ULZ1qnI1Ww5\n" +
>             
> "qUbjrJcVv2rWHeqS5xmJiyuQq2+xqVijyMHAfb/0O/2imSINOYuCGq7tBsHpG3rc\n" +
>             
> "k8fpB7Q43+aZRnQNN6F12GwEH/vTSkhbWABSUzLjGxNc1JzArGqhp8Fm2UxuZ+gG\n" +
>             
> "rtfnCQp3AaYrZa1SAxFnA7cfSWiLYv6gv2ejpekbN9Z/iff71in6a0wNOMofbvOv\n" +
>             
> "wLuIuorjHynpc21brJs09Rx4uxJv3kyC8keoWcM22oECgYEA7BwqquZbRAE4EmNM\n" +
>             
> "vWSeXeMnMfQy9u786WoXv3PW7J6ftniKf6f0ZaIdHJv4ilgFf6Xk5ap8e4S2x21W\n" +
>             
> "ciTH/u34O/UTmQ7t5UkGscZzcCldO4vPoU1531md4hXtMcY8W02lRfMtvV9SmlR0\n" +
>             
> "I39eJU0NLb70KgQZ060FUNnL8c0CgYEAzjBQ5IA51KGUOQA81cPrhuUat1/sExgQ\n" +
>             
> "lxsfn5ZW4S74G7fl8W5n6LfX1kZryj11XYEx0Vv3kPuFQ6pjjyv5YiSi7ZgCtD+8\n" +
>             
> "lZcHeadV9V7Mm77x4+pTlJ1ZuMb575OxKOfEX0LmN6qqvW0U5LdSCDa1WWkIqHaa\n" +
>             
> "/MVAM22QOFcCgYEA4PaZZMolTS9IKKT6Wj4DcntbPgppgMQGr7NJOz55GmysyiQh\n" +
>             
> "+i2h/DAxQrANaGsjmhMLfBQrlVjG+k7gHdOTxv8gFKiW6q/B1UP2H+5w0P5oebLl\n" +
>             
> "us/h/gAaIW8418MEgQ4DGhnwi83GG4u6OJRDtJCsrNiTNXFA1mG1fep2mkUCgYEA\n" +
>             
> "k92UdXn7fyBtIr+n4QlC5BdzJGSW8U6Fv0fFUvZG0fCUH5SvQ4gQ3pTRJaqU7JFM\n" +
>             
> "lMTtDB4vGXs3I8KS6X74tkhdy5QDBG7c+E46HyVBANl+VIcIA5HtZJu/V0LixMwe\n" +
>             
> "9Z3YdxSL8wnirjwHCsro+lj5juhDPETqezGeDAObtLsCgYEAqQQvzv/zc/HmzdhI\n" +
>             
> "wvFg8HubQWOhv7LwF16VBLA1I7ojZnTCvIQph9GKmp6NQRY1oHUC62giYDZzCjQ3\n" +
>             
> "2lAYmJ52/vIep2ktoa/tAxJdCLL0ULkpfze8ZJSNN3uSip4KadybAwz7JHuQ9J/5\n" +
>             "XKr8FjgTA/zAHLJ9Dft+jqlx9LA=\n" +
>             "-----END PRIVATE KEY-----\n";
>     private final static String CERT_PEM = "" +
>             "-----BEGIN CERTIFICATE-----\n" +
>             
> "MIIDDTCCAfWgAwIBAgIUPKaQbQfY5d36BzcHcJg5MmMaqBQwDQYJKoZIhvcNAQEL\n" +
>             
> "BQAwFjEUMBIGA1UEAwwLdGVzdC1jbGllbnQwHhcNMTkwMzI5MTg0NzI0WhcNMjkw\n" +
>             
> "MzI2MTg0NzI0WjAWMRQwEgYDVQQDDAt0ZXN0LWNsaWVudDCCASIwDQYJKoZIhvcN\n" +
>             
> "AQEBBQADggEPADCCAQoCggEBAL4rOjkSF3W+tt5lr6s0iqcjNYDMqv+JYXJJCcZC\n" +
>             
> "7lKkrWexnkmEErzcnhFDFFLqZQuuJ8mAb8CoyUDQ2YIPDQLqn+5AX5UrJye1QuOe\n" +
>             
> "6/CNTTiWROataTe3Aee8OR36PZIf6N/ONl4+/6VcIJAtBLnGIyt4aMzHgQgtSwHT\n" +
>             
> "QRChQVt0aDU2BO3uDwv9a4QML1jJsLoUkc4Fg//TbCZbhERjIy5VjdJ9kCw6TlhF\n" +
>             
> "sR/blzsgADf61tpfP1K5GBQrdIojRF4YG0vK9KODKjI5PBPz1A50Swvooz1NmGMk\n" +
>             
> "amZ5cYwcE0/u4Re5eB8HrAprLV6MPqGv6VyCcgJCoe2EBKsCAwEAAaNTMFEwHQYD\n" +
>             
> "VR0OBBYEFHVu5hu3Z7NYqZuxroKghvqtlItSMB8GA1UdIwQYMBaAFHVu5hu3Z7NY\n" +
>             
> "qZuxroKghvqtlItSMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB\n" +
>             
> "AKPfRWU4W6Zr/HV3hGYIga/arXWpkO/2mW0dN9qdQI3Ok1rXezFhrtxiR5iOaN0f\n" +
>             
> "wYKAUAbfQmfN6Ai4Nl0ZSbuWRe7/Vut/+rwINaARZLoDz3HVeAmi52ZKsikZG9h0\n" +
>             
> "InpDa52Zs/8nnxPo7UiXP05mMTt2psyQGYeolac2FliKchZ7+SvX2FPxSUQ7LkNT\n" +
>             
> "1DGu7jlLkT3plf9rCW0PhIa6vpzyxVVijjqZl86fBU6vaFzJHH6pNoPpQbK4vOmb\n" +
>             
> "2278AHjrDTU6j8IL0uSk8g2klZ8hC9kerblLREp09Haw5l74kwBiypLHV366XbWR\n" +
>             "uDXRs4Y1a29pJ41jDhlqllk=\n" +
>             "-----END CERTIFICATE-----\n";
>     public static void main(String[] args) throws Exception {
>         final X509Certificate clientCertificate = 
> PemUtils.loadCertificate(CERT_PEM);
>         final PrivateKey clientKey = PemUtils.loadPrivateKey(KEY_PEM);
>         final SSLContextBuilder sslContextBuilder = SSLContexts.custom();
>         final KeyStore keystore = 
> KeyStore.getInstance(KeyStore.getDefaultType());
>         keystore.load(null, null);
>         // When this line is commented out, no client certificate is used and 
> Keep-Alive works as expected
>         // and connection gets reused for the second request.
>         // However when line is not commented, client SSL cert is used and 
> this breaks connection pool logic
>         keystore.setKeyEntry("client", clientKey, new char[]{}, new 
> X509Certificate[]{clientCertificate});
>         sslContextBuilder.loadKeyMaterial(keystore, new char[]{});
>         final SSLConnectionSocketFactory sslConnectionSocketFactory = new 
> SSLConnectionSocketFactory(
>                 // Do not verify hostname in case we want to use IP in 
> addition to name.
>                 // After all this is a test only
>                 sslContextBuilder.build(), new NoopHostnameVerifier());
>         final Registry<ConnectionSocketFactory> socketFactoryRegistry =
>                 RegistryBuilder.<ConnectionSocketFactory>create()
>                         .register("https", sslConnectionSocketFactory)
>                         .build();
>         PoolingHttpClientConnectionManager connManager = new 
> PoolingHttpClientConnectionManager(socketFactoryRegistry);
>         final CloseableHttpClient client = HttpClients.custom()
>                 .setConnectionManager(connManager)
>                 .setDefaultRequestConfig(
>                         RequestConfig.custom()
>                                 .setRedirectsEnabled(false)
>                                 .build())
>                 .build();
>         // Found the server here https://stackoverflow.com/a/42770043 - it 
> requires client certificate
>         // Any "normal" HTTPS server like google.com does not ask for 
> certificate
>         // and the issue is only reproducible when client cert is actually 
> used not just added to KeyStore
>         final String url = "https://server.cryptomix.com/secure/";;
>         // Actual responses are irrelevant. The important bit is what tcpdump 
> shows:
>         //   sudo tcpdump -pn host server.cryptomix.com
>         // there will be two separate connections for each
>         // And if client cert is not used, there will be only one
>         final CloseableHttpResponse response1 = client.execute(new 
> HttpGet(url));
>         System.out.println("response1 = " + 
> response1.getHeaders("Content-Type")[0]);
>         EntityUtils.consume(response1.getEntity());
>         final CloseableHttpResponse response2 = client.execute(new 
> HttpGet(url));
>         System.out.println("response2 = " + 
> response2.getHeaders("Content-Type")[0]);
>         EntityUtils.consume(response2.getEntity());
>         // The fact two connections were used is also visible in the pool 
> stats
>         // as it shows
>         //   [leased: 0; pending: 0; available: 2; max: 20]
>         // and when keystore.setKeyEntry() is commented out - just 1 
> available connection
>         System.out.println("Pool stats: " + connManager.getTotalStats());
>     }
> }
> {code}



--
This message was sent by Atlassian JIRA
(v7.6.3#76005)

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to