Chris Lott
2016-09-14 21:22:26 UTC
I'd like to ask about a behavior I see in CXF v 3.0.10 and v 3.1.7. I'm
using JDK 1.8, and I have a tiny test program (see below). Our REST
service is provided by Apache HTTPD (fronting Tomcat). The HTTPD is
configured with 2 virtual hosts that differ only in name. A conforming
client that sends SNI information works perfectly - receives the
appropriate certificate. The CXF client does not work - throws an
exception like this:
Caused by: java.io.IOException: HTTPS hostname wrong: should be
<my-host-qa.my.company.com>
at
sun.net.www.protocol.https.HttpsClient.checkURLSpoofing(HttpsClient.java:649)
at
sun.net.www.protocol.https.HttpsClient.afterConnect(HttpsClient.java:573)
at
sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:185)
at
sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1513)
at
sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1441)
at
java.net.HttpURLConnection.getResponseCode(HttpURLConnection.java:480)
at
sun.net.www.protocol.https.HttpsURLConnectionImpl.getResponseCode(HttpsURLConnectionImpl.java:338)
at
org.apache.cxf.transport.http.URLConnectionHTTPConduit$URLConnectionWrappedOutputStream.getResponseCode(URLConnectionHTTPConduit.java:332)
at
org.apache.cxf.transport.http.HTTPConduit$WrappedOutputStream.doProcessResponseCode(HTTPConduit.java:1581)
at
org.apache.cxf.transport.http.HTTPConduit$WrappedOutputStream.handleResponseInternal(HTTPConduit.java:1610)
at
org.apache.cxf.transport.http.HTTPConduit$WrappedOutputStream.handleResponse(HTTPConduit.java:1551)
at
org.apache.cxf.transport.http.HTTPConduit$WrappedOutputStream.close(HTTPConduit.java:1348)
... 12 more
In a nutshell, the CXF library does not seem to provide Server Name
Indication (SNI) to the remote HTTPS server. Note that fetching
application/json content using a very plain Java method works just fine
- it provides SNI and gets the appropriate certificate - so I do not
believe it's a limitation of the platform that I'm using.
Is there some additional configuration I need to do in this sample
client?
Thanks for listening.
---
package com.mycompany.ecomp;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URL;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import javax.net.ssl.HttpsURLConnection;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.apache.cxf.jaxrs.client.WebClient;
/**
* Trivial Java REST client to test whether SNI is provided when HTTPS
content
* is fetched. Tested with Apache CXF ver 3.0.10 and 3.1.7.
*/
public class FetchRestContent {
class HttpResponse {
int code;
String body;
}
/**
* Fetches REST content using only features provided by standard Java
* libraries.
*
* @param url
* @param user
* Set as header parameter "username"
* @param pass
* Set as header parameter "password";
* @return Code and body fetched from remote server
* @throws Exception
* on any failure
*/
private HttpResponse getRestContentJdk(String uriString, String path,
String user, String pass) throws Exception {
URL url = new URL(uriString + '/' + path);
HttpsURLConnection con = (HttpsURLConnection) url.openConnection();
if (con == null)
throw new Exception("Failed to open HTTPS connection");
con.setRequestMethod("GET");
con.setConnectTimeout(2000);
con.setReadTimeout(2000);
con.setRequestProperty("username", user);
con.setRequestProperty("password", pass);
// Throws an exception on an HTTPS connection if the
// local trust store is not configured appropriately to
// accept the self-signed certificate at the remote end.
HttpResponse resp = new HttpResponse();
resp.code = con.getResponseCode();
BufferedReader in = new BufferedReader(new
InputStreamReader(con.getInputStream(), "UTF-8"));
Certificate[] certs = con.getServerCertificates();
for (Certificate cert : certs) {
if (cert instanceof X509Certificate) {
X509Certificate xcert = (X509Certificate) cert;
System.out.println("X509 Principal name " +
xcert.getSubjectX500Principal().getName());
}
}
StringBuffer sb = new StringBuffer();
String inputLine;
while ((inputLine = in.readLine()) != null)
sb.append(inputLine);
in.close();
con.getInputStream().close();
con.disconnect();
resp.body = sb.toString();
return resp;
}
/**
* Fetches REST content using Jax-RS, as implemented by Apache CXF.
*
* @param uri
* @param path
* @param user
* @param pass
* @return
* @throws Exception
*/
private HttpResponse getRestContentJaxrs(String uriString, String path,
String user, String pass) throws Exception {
URI uri = new URI(uriString);
WebClient client = WebClient.create(uri);
client.type(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON);
client.path(path);
Response response = client.get();
HttpResponse httpResponse = new HttpResponse();
httpResponse.code = response.getStatus();
httpResponse.body = response.readEntity(String.class);
return httpResponse;
}
public static void main(String[] args) throws Exception {
if (args.length != 4)
throw new IllegalArgumentException("Expect 4 arguments: URL path
username password");
if (!args[0].toLowerCase().startsWith("https"))
throw new IllegalArgumentException("Expect https prefix");
FetchRestContent fetcher = new FetchRestContent();
System.out.println("GET-ing content via Java from " + args[0] + ",
path " + args[1]);
HttpResponse r1 = fetcher.getRestContentJdk(args[0], args[1], args[2],
args[3]);
System.out.println("HTTP response code is " + r1.code);
System.out.println("Response body follows:");
System.out.println(r1.body);
System.out.println("GET-ing content via Jax-RS from " + args[0] + ",
path " + args[1]);
HttpResponse r2 = fetcher.getRestContentJaxrs(args[0], args[1],
args[2], args[3]);
System.out.println("HTTP response code is " + r2.code);
System.out.println("Response body follows:");
System.out.println(r2.body);
}
}
using JDK 1.8, and I have a tiny test program (see below). Our REST
service is provided by Apache HTTPD (fronting Tomcat). The HTTPD is
configured with 2 virtual hosts that differ only in name. A conforming
client that sends SNI information works perfectly - receives the
appropriate certificate. The CXF client does not work - throws an
exception like this:
Caused by: java.io.IOException: HTTPS hostname wrong: should be
<my-host-qa.my.company.com>
at
sun.net.www.protocol.https.HttpsClient.checkURLSpoofing(HttpsClient.java:649)
at
sun.net.www.protocol.https.HttpsClient.afterConnect(HttpsClient.java:573)
at
sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:185)
at
sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1513)
at
sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1441)
at
java.net.HttpURLConnection.getResponseCode(HttpURLConnection.java:480)
at
sun.net.www.protocol.https.HttpsURLConnectionImpl.getResponseCode(HttpsURLConnectionImpl.java:338)
at
org.apache.cxf.transport.http.URLConnectionHTTPConduit$URLConnectionWrappedOutputStream.getResponseCode(URLConnectionHTTPConduit.java:332)
at
org.apache.cxf.transport.http.HTTPConduit$WrappedOutputStream.doProcessResponseCode(HTTPConduit.java:1581)
at
org.apache.cxf.transport.http.HTTPConduit$WrappedOutputStream.handleResponseInternal(HTTPConduit.java:1610)
at
org.apache.cxf.transport.http.HTTPConduit$WrappedOutputStream.handleResponse(HTTPConduit.java:1551)
at
org.apache.cxf.transport.http.HTTPConduit$WrappedOutputStream.close(HTTPConduit.java:1348)
... 12 more
In a nutshell, the CXF library does not seem to provide Server Name
Indication (SNI) to the remote HTTPS server. Note that fetching
application/json content using a very plain Java method works just fine
- it provides SNI and gets the appropriate certificate - so I do not
believe it's a limitation of the platform that I'm using.
Is there some additional configuration I need to do in this sample
client?
Thanks for listening.
---
package com.mycompany.ecomp;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URL;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import javax.net.ssl.HttpsURLConnection;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.apache.cxf.jaxrs.client.WebClient;
/**
* Trivial Java REST client to test whether SNI is provided when HTTPS
content
* is fetched. Tested with Apache CXF ver 3.0.10 and 3.1.7.
*/
public class FetchRestContent {
class HttpResponse {
int code;
String body;
}
/**
* Fetches REST content using only features provided by standard Java
* libraries.
*
* @param url
* @param user
* Set as header parameter "username"
* @param pass
* Set as header parameter "password";
* @return Code and body fetched from remote server
* @throws Exception
* on any failure
*/
private HttpResponse getRestContentJdk(String uriString, String path,
String user, String pass) throws Exception {
URL url = new URL(uriString + '/' + path);
HttpsURLConnection con = (HttpsURLConnection) url.openConnection();
if (con == null)
throw new Exception("Failed to open HTTPS connection");
con.setRequestMethod("GET");
con.setConnectTimeout(2000);
con.setReadTimeout(2000);
con.setRequestProperty("username", user);
con.setRequestProperty("password", pass);
// Throws an exception on an HTTPS connection if the
// local trust store is not configured appropriately to
// accept the self-signed certificate at the remote end.
HttpResponse resp = new HttpResponse();
resp.code = con.getResponseCode();
BufferedReader in = new BufferedReader(new
InputStreamReader(con.getInputStream(), "UTF-8"));
Certificate[] certs = con.getServerCertificates();
for (Certificate cert : certs) {
if (cert instanceof X509Certificate) {
X509Certificate xcert = (X509Certificate) cert;
System.out.println("X509 Principal name " +
xcert.getSubjectX500Principal().getName());
}
}
StringBuffer sb = new StringBuffer();
String inputLine;
while ((inputLine = in.readLine()) != null)
sb.append(inputLine);
in.close();
con.getInputStream().close();
con.disconnect();
resp.body = sb.toString();
return resp;
}
/**
* Fetches REST content using Jax-RS, as implemented by Apache CXF.
*
* @param uri
* @param path
* @param user
* @param pass
* @return
* @throws Exception
*/
private HttpResponse getRestContentJaxrs(String uriString, String path,
String user, String pass) throws Exception {
URI uri = new URI(uriString);
WebClient client = WebClient.create(uri);
client.type(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON);
client.path(path);
Response response = client.get();
HttpResponse httpResponse = new HttpResponse();
httpResponse.code = response.getStatus();
httpResponse.body = response.readEntity(String.class);
return httpResponse;
}
public static void main(String[] args) throws Exception {
if (args.length != 4)
throw new IllegalArgumentException("Expect 4 arguments: URL path
username password");
if (!args[0].toLowerCase().startsWith("https"))
throw new IllegalArgumentException("Expect https prefix");
FetchRestContent fetcher = new FetchRestContent();
System.out.println("GET-ing content via Java from " + args[0] + ",
path " + args[1]);
HttpResponse r1 = fetcher.getRestContentJdk(args[0], args[1], args[2],
args[3]);
System.out.println("HTTP response code is " + r1.code);
System.out.println("Response body follows:");
System.out.println(r1.body);
System.out.println("GET-ing content via Jax-RS from " + args[0] + ",
path " + args[1]);
HttpResponse r2 = fetcher.getRestContentJaxrs(args[0], args[1],
args[2], args[3]);
System.out.println("HTTP response code is " + r2.code);
System.out.println("Response body follows:");
System.out.println(r2.body);
}
}